并发
1.多线程基础:
1.进程:正在进行中的程序(直译),程序的一次执行,线程引入之前,进程是资源持有的小单位,也是程序运行的小单元。
2.线程:就是进程中一个负责程序执行的控制单元(执行路径)。
3.并行:是物理层面的,是同时发生多个并发事件。
4.并发:不一定同时间完成多件事情,是CPU切换完成,是逻辑上的同时发生,多线程不能跨系统。
多线程的好处:解决了多部分同时运行的问题。
多线程的弊端:线程太多会导致效率的降低。(即是速度的放慢) 其实应用程序的执行都是cpu在做着快速的切换,这个切换是随机的。 JVM(虚拟机)在启动时候就启动了多个线程,至少有五个线程运行。
线程实现的方式:
1,继承Thread类。
2,实现Runnable接口。
3,实现Callable接口。 其中Callable,的run方法可以有返回值,在开启线程的时候,需要通过FutureTask将Callable封装成一个Runnable才可以执行。
线程安全问题产生的原因:
1,多个线程同时操作一个共享数据。
2,线程之前竞争同一把锁。
解决思路:
就是把多条操作共享数据的代码封装起来,当一个线程在执行这些代码的时候,规定其他线程不能参与运算。 必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
1,用synchronized同步代码块
2,用synchronized同步函数
同步函数和同步代码块的区别:
1,同步函数的锁是对象锁或者类锁。
2,同步代码快的锁是任意对象。 建议使用同步代码快。
注意:同步函数不能由static修饰,一旦修饰了,静态中没有this,同步函数就没有同步锁this了。静态同步函数所使用的同步锁是该函数所属的字节码文件对象。 可以用this.Class()表示,也可以用类名.class表示。
sychronized中的锁机制:
JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。 代码块同步是使用monitorenter和monitorexit指令实现,而方法 同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明,但是方法的同步同样可以使用这两个指令来实现。 monitorenter指令是在编译后插入到同步代码块的开 始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。 任何对象都有一个monitor与之关联,当且一 个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
2.Monitor: 什么是Monitor?
我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生 的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
锁优化: 在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、 偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,
他们会 随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
3.对象锁(synchronized method{})和类锁(static sychronized method{})的区别:
1.对象锁也叫实例锁,对应synchronized关键字,当多个线程访问多个实例 时,它们互不干扰,每个对象都拥有自己的锁,如果是单例模式下,那么就是变成和类锁一样的功能。对象锁防止在同一个时刻多个线程访问同一个对象的synchronized块。如 果不是同一个对象就没有这样子的限制。对象锁锁的是当前对象的实例。 类锁对应的关键字是static
2.sychronized,是一个全局锁,无论多少个对象否共享同一个锁(也可以 锁定在该类的class上或者是classloader对象上),同样是保障同一个时刻多个线程同时访问同一个synchronized块,当一个线程在访问时,其他的线程等待。类锁锁的是 当前对象的Class对象,只有一个Class对象。
总结:
1.类锁是对静态方法使用synchronized关键字后,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的。
2.对象锁是实例方法使用synchronized关键 字后,如果是多个线程访问同个对象的sychronized块,才是同步的,但是访问不同对象的话就是不同步的。
3.类锁和对象锁是两种不同的锁,可以同时使用,但是注意类锁不 要嵌套使用,这样子容易发生死锁。
4,对象锁,锁的是类的对象实例。类锁,锁的是每个类的的Class对象,每个类的的Class对象在一个虚拟机中只有一个,所以类锁也只有一个。 线程同步之后,会影响效率。
4.使变量可见的关键字:volatile:
适合于只有一个线程写,多个线程读的场景,因为它只能确保可见性。
原理:
缓存一致性协议,如果对声明了volatile的变量进行写操 作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就 会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期 了(类似于乐观锁),当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内 存中把数据读到处理器缓存里。
注意:
volatile保证可见性,是保证所有指令执行完了之后的值是可见的,指令执行的过程中是没有其他线程来修改这个值,但是如果多个线程 在对这个变量进行写操作的时候,在A线程将主内存中的变量值修改的过程中(指令执行的过程中),B线程又修改了主内存中的值,这时候等A线程将修改后的值放回主内存的时 候,主内存中就有两个变量的值,这时候就混乱了,这种情况就不能保证数据的原子性。
使用场景:
一个线程写,多个线程读的情况。
并发专家建议我们远离volatile是有道理的,这里再总结一下:
1,volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。
2,如今非volatile的共享变量,在 访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。
3,volatile不能保证原子性。
4,volatile会禁止指令重排序。
5.ThreadLocal
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。
ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal内部就是一个ThreadLocalMap。
ThreadLocal是如何为每个线程创建变量的副本的:
1,首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals, 这个threadLocals就是用来存储实际的变量副本的,键值为当 前ThreadLocal变量(即是对应着当前线程),value为变量副本(即Entry类型的变量)。
2,初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调 用get()方法或者set()方法, 就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value, 存到threadLocals。
3,然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
总结一下:
1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
2)为何threadLocals的类型ThreadLocalMap的键值 为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
3)在进行get之前,必须先set,否则会报空指针异常; 如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。因为我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialVau方法返回i,而在setInitialValue方法中,有一个语句是T value =initialValue(),而默认情况下,initialValue方法返回的是null。
4)因为会为每个线程分配一个副本,所以不宜存储比较大的对象,消耗内存空间。
5)ThreadLocal遇上线程池的问题及解决办法:
线程池中的线程在任务执行完成后会被复用,所以在线程执行完成时,要对ThreadLocal行清理(清除掉与本线程相关联的value 对象)。不然,被复用的线程去执行新的任务时会使用被上一个线程操作过的value对象,从而产生不符合预期的结果,会产生内存泄漏。
6.指令重排序(编译器的一种优化),当数据存在依赖性的时候就不会发生指令重排序。
依赖性:
1,数据依赖性
2,控制依赖性
解决:as-if-serial规则(单线程下的)
因为重排序,产生了Happens-Before规则(多线程下的) Happens-Before: the first is visible to and ordered before the second 其实就是,对于程序员而言,以为指令的执行顺序是从上往下的,但是对于CPU而言,为了提高效率,会进行重排序,后一个指令执行顺序在前一个 指令之前,但是前一个执行的结果又对后一个指令是可见的,所以结果是不会产生错误的。
中断:
interrupt(只是告诉线程要中断了,但是线程并不会马上中断)
在发生中断异常的时候,会自动将中断状态变成false start(进入就绪状态) wait:(进入阻塞状态,会释放锁)–需要被唤醒(一般使用notifyALL,而不使用notify,因为notify并不能唤醒指定的线程,一般唤醒等待队列中的第一个线程,如果唤醒 错了线程,容易造成死锁。) 线程进入等待队列,是一个单向链表。
notify,notifyAll:(不立即释放锁,必须等到所在同步块执行完之后才释放锁。)
sleep:(释放执行权,不会释放锁,由执行变为阻塞)sleep()方法不考虑线程优先级,所有的线程都有机会执行。
yield:(释放执行权,不会释放锁,由执行变为就绪)yield()方法只能使同优先级或者高优先级的线程得到执行机会。
join:是当前线程释放执行权,将执行权交给被调用join的线程,当被调用join的线程执行完了之后,当前线程才获取到执行权。
线程挂起:
挂起:一般是主动的,由系统或程序发出,甚至于辅存中去。(不释放CPU,可能释放内存,放在外存,可以理解为,当前线程正在运行中,突然被CPU切换出去,等会再由CPU切 换回来运行。)
阻塞:一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待某种资源或信号量(即有了资源)将他唤醒。(释放CPU,不释放内存)
7.Fork/Join:
是一个分而治之的任务框架,如一个任务需要多线程执行,分割成很多块计算的时候,可以采用这种方法。
动态规范:和分而治之不同的是,每个小任务之间互相联系。
工作密取:分而治之分割了每个任务之后,某个线程提前完成了任务,就会去其他线程偷取任务来完成,加快执行 效率。同时,第一个分配的线程是从队列中的头部拿任务,当完成任务的线程去其他队列拿任务的时候是从尾部拿任务,所以这样就避免了竞争。
在Java的Fork/Join框架中,使用两个类完成上述操作:
1.ForkJoinTask:我们要使用Fork/Join框架,首先需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情 况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:
a.RecursiveAction:用于没有返回结果的任务
b.RecursiveTask:用于有返回结果的任务
2.ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行.他其实也是一个线程池。它使用了一个无限队列 来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。
注意:ForkJoinPool的invoke方法是同步阻塞的,excute方法是异步的。
Fork/Join框架的实现原理:
ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交 给ForkJoinPool,而ForkJoinWorkerThread负责执行这些任务。
使用场景: Fork/Join框架适合能够进行拆分再合并的计算密集型(CPU密集型)任务。ForkJoin框架是一个并行框架,因此要求服务器拥有多CPU、多核,用以提高计算能力。 如果是单核、单CPU,不建议使用该框架,会带来额外的性能开销,反而比单线程的执行效率当然不是因为并行的任务会进行频繁的线程切换,因为Fork/Join框架在进行线程池初始 化的时候默认线程数量为Runtime.getRuntime().availableProcessors(),单CPU单核的情况下只会产生一个线程,并不会造成线程切换,而是会增加Fork/Join框架 的一些队列、池化的开销。
比如:数据迁移到数据库,解析excel等等可以拆分完成的任务都可以使用到forkjoin。
8.常用的并发工具类: CountDownLatch:
作用:是一组线程等待其他的线程完成工作以后在执行,加强版join,await用来等待,countDown负责计数器的减一 CyclicBarrier: 让一组线程达到某个屏障,被阻塞,一直到组内后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行CyclicBarrier(int parties),CyclicBarrier(int parties, Runnable barrierAction),屏障开放,barrierAction定义的任务会执行
CountDownLatch和CyclicBarrier辨析:
1、countdownlatch放行由第三者 控制,CyclicBarrier放行由一组线程本身控制
2、countdownlatch放行条件》=线程数,CyclicBarrier放行条件=线程数
3、CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
4、CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使 用reset()方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
Semaphore: 控制同时访问某个特定资源的线程数量,用在流量控制,一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程 将被唤醒并再次试图获得信号量。
Exchange: 两个线程间的数据交换。
Future:对于多线程,如果线程A要等待线程B的结果,那么线程A没必要等待B,直到B有结果,可以先拿到一个未来的Future,等B有结果是再取真实的结果。 Future的核心思想是:一个方法f,计算过程可能非常耗时,等待f返回,显然不明智。可以在调用f的时候,立马返回一个Future,可以通过Future这个数据结构去控制方法f的计算过程。 FutureTask是Future的实现类。
当线程数量不确定的时候,就使用Future,因为future的get方法会等待线程执行之后,其他线程才会继续执行。
让一个线程等待另一个线程执行完之后再执行:
1,CountDownLatch
2,join方法
3,使用Callable,调用FutureTask的get方法
9.CAS:
CAS有3个操作数,内存地址值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时(比较的是地址引用),将内存值V修改为B,否则返回V。 CAS是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,循环这个指令,直到成功为止。
原子操作类:AtomicInteger等,核心就是CAS(CompareAndSwap) 原子操作类的使用,当高并发的情况下,对于基本数据类型或者引用数据类型的操作,避免多线程问题的处理方式一般有 加锁,但是加锁会影响性能,所以这个时候可以考虑使用原子操作类。CAS由于是在硬件方面保证的原子性,不会锁住当前线程,所以执行效率是很高的。 注意:原子操作和锁是 一样的一种可以保证线程安全的方式,如何让线程安全就看如何使用锁或者如何使用原子操作CAS使用了正确的原子操作,所以保证了线程安全。
好处:保证了数据的原子性
坏处:
1,ABA问题(并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。 可以使用乐观锁的方式解决。)
2,循环时间长开销大(自旋)
3,只能保证一个共享变量的原子操作(可以使用AtomicRefrence原子操作类将多个变量合并成一个对象来解决)
解决ABA问题: AtomicMarkableReference,内部是一个boolean类型的版本号,可以记录是否被更改过。 AtomicStampedReference,内部是一个int类型的版本号,可以记录被更改的次数。
10.显示锁:
Lock Lock接口和synchronized的比较:
1,synchronized代码更简洁
2,Lock,效率比隐士锁更高
3,Lock可以在获取锁可以被中断,超时获取锁,尝试获取锁 Lock接口有三个实现类:一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。 ReentrantReadWriteLock类实 现了ReadWriteLock接口。
锁的公平和非公平: 如果在时间上,先对锁进行获取的请求,一定先被满足,这个锁就是公平的,不满足,就是非公平的,意思就是,线程等待队列中的优先级高的线程先获取到锁,就是公平锁,没有获取到锁,就是非公平锁,非公平的效率一般来讲更高,因为非公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。
ReentrantLock(可重入锁,构造函数可以指定是否是公平锁,默认是非公平锁):
可重入锁:一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。
不可重入锁:即当前线程获取这把锁后,要想再拿到这把锁,必须释放当前持有的锁,这时我们称这个锁是不可重入的。 ReentrantLock内部实现了一个非公平锁和公平锁:
非公平锁(NonfairSync):
继承Sync(内部实现了AQS的锁),其中在tryAcquire方法中,判断如果是当前线程,就会将state状态值一直累加,实现锁的可重入,同时,在释放的时候依次递减状态值
公平锁(fairSync):继承Sync(内部实现了AQS的锁),在获取锁的时候,首先根 据hasQueuedPredecessors方法判断当前节点有没有前驱节点,如果有的话,就将当前线程设为挂起状态,其他跟非公平锁实现一致。
ReentrantLock和Syn关键字:都是排他锁,同一时刻只允许一个线程访问。
ReentrantReadWriteLock读写锁:
读写锁实际是一种特殊的自旋锁,同一时刻允许多个读线程同时访问,但是写线程访问的时候,所有的读和写都被阻塞,适宜与读多写少的情况。读写锁在内部有两个实现了Lock接口的静态内部类,读锁和写锁。
同步状态state: 同步状态由一个整型变量表示,因为这个变量需要表示多个线程的读和写的状态,因此读写锁在实现上将该变量的高16位表示读,低16位表示写,其中每位的读线程重入的次数是 由HoldCounter对象包装之后放入到ThreadLocal中。
注意:
1,在读多写少的情况下,性能比一般的排他锁和Syn关键字要高。
2,当有读锁时,写锁就不能获得,而当有写锁时,当前线程还可以获取读锁,其他线程不能取读锁,同一线程可以保证数据可见性。
如果持有读锁的时候去获取写锁,这就是所谓锁的升级,这是不允许的,因为读锁可能有多个线程获取了,如果允许获得写锁,那就真正可能产生可见性性问题。注意:(可见性是针对不同线程而言)
3,锁降级:锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
原理:
锁降级存在争议,在很多本书里面出现过,锁降级中,读锁的获取的目的是“为了保证数据的可见性”。而得到这个结论的依据是“如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程 T 的数据更新”。这里貌似有个漏洞:如果另一个线程获取了写锁(并修改了数据),那么这个锁就被独占了,没有任何其他线程可以读到数据,更不用谈“感知数据更新”。所以,主要是性能上的优化,如果先释放写锁,再获取读锁,势必引起锁的争抢和线程上下文切换,影响性能,所以通过锁降级,可以避免锁的竞争。
4,锁升级:在没有释放读锁的情况下,就去申请写锁,这属于锁升级,ReentrantReadWriteLock是不支持的。
5,读锁本质上是个共享锁。但读锁对锁的获取做了很多优化,比如:
1,使用firstReader和cachedHoldCounter对第一个读锁线程和后一个读锁线程做优化,优化点主要在释放的时候对计数器的获取,其他获取读锁的线程就放在HoldCounter中。
2,同时,如果在获取读锁的过程中写锁被持有,JUC并没有让所有线程痴痴的等待,而是判断入如果获取读锁的线程是正巧是持有写锁的线程,那么当前线程就可以降级获取写锁,否则就会死锁了(为什么死锁,当持有写锁的线程想获取读锁,但却无法降级,进入了等待队列,肯定会死锁)。
3,还有一点就是性能上的优化,如果先释放写锁,再获取读锁,势必引起锁的争抢和线程上下文切 换,影响性能。
6,在读锁和写锁的获取过程中支持中断。
7,提供确定锁是否被持有等辅助方法。
显示锁中的条件接口Condition,包含如等待和唤醒之类的方法,Condition对象是由Lock对象调用Lock对象的newCondition()方法)创建出来的,换句话 说,Condition是依赖Lock对象的。 在显示锁中,唤醒是用signal,而好不用signalAll,因为signal可以精准唤醒指定的线程,而在隐士锁中用notifyAll,而不用notify,因为notify不能精准唤醒,可能导致死锁。signalAll只能唤醒指定Condition上的等待的线程,其他线程也不能被唤醒,和notifyAll不同。
11.LockSupport:
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞,实现的阻塞和解除阻塞是基于”许可(permit)”作为关联,permit相当于一个信号量(0,1),默认是0.线程之间不再需要一个Object或者其它变量来存储状态,不再需要关心对方的状态.(和Semaphore有些许区别,许可证不会累加,多只有一张)。
总结 简而言之:
1.实现机制和wait/notify有所不同,面向的是线程
2.不需要依赖监视器
3.与wait/notify没有交集
4.使用起来方便灵活
12.AQS:
AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReenrantLock/Semaphore/CountDownLatch…,AQS采用的是模板方法模式。 AQS中有独占锁和共享锁,重入锁。
AQS中的模板方法:
独占式获取: accquire acquireInterruptibly tryAcquireNanos
共享式获取: acquireShared acquireSharedInterruptibly tryAcquireSharedNanos
独占式释放锁: release 共享式释放锁: releaseShared
需要子类覆盖的流程方法:
独占式获取:tryAcquire
独占式释放:tryRelease
共享式获取:tryAcquireShared
共享式释放:tryReleaseShared
这个同步器是否处于独占模式:isHeldExclusively
同步状态state(标志的当前线程是否获取到锁):
getState:获取当前的同步状态
setState:设置当前同步状态,不能保证原子性
compareAndSetState:使用CAS设置同步状态,保证状态设置的原子性
独占锁(类似于ReentrantLock和ReentrantReadWriteLock里的写锁都是独占锁):定义一个state,如果是1就是拿到了锁,反之则没有,独占锁就是一个排它锁。 共享锁(类似于ReentrantReadWriteLock里的读锁是共享锁): 定义state是很多个锁的数量,拿到一个锁就state–,释放一个就state++,Semaphore的实现就是 参照共享锁。
重入锁:
比如一个线程拿到一个锁,在run中又调用了其他同步方法,如果不能重入,一个线程就要拿到两个锁,这样就不行。
AQS的数据结构–双向链表:
1,节点Node
竞争失败的线程会打包成Node放到同步队列,Node可能的状态里:
CANCELLED:线程等待超时或者被中断了,需要从队列中移走
SIGNAL:后续的节点处于等待状态,当前节点被释放了,通知后面的节点去运行
CONDITION:当前节点处于等待队列中
PROPAGATE:共享,表示状态要往后面的节点传播
0:表示初始状态
2,FIFO同步队列
AQS的原理就是:当一个锁线程获取锁失败的时候,会将这个线程封装成一个节点Node,通过addWaiter方法放入到FIFO同步队列中的尾部(这一步是试用CAS操作,保证原子性),然后再通过acquireQueued方法自旋从同步队列的头部依次获取锁,如果其中某个获取锁失败,就继续封装成一个Node放入队列的尾部,直到获取锁返回。
比如:当前 有10个线程去竞争锁,有一个拿到了锁,另外9个就会被依次封装成一个Node节点,按照线程优先级依次从尾部放入到同步队列中,用节点的前驱和后驱连接起来行程双向链表,然后当获取锁的线程释放了锁之后,就会依次从头部去唤醒线程。
注意:
1,在增加尾节点的时候,是采用的CAS,因为在竞争锁的时候可能是多个线程,后有多个线程失败需要进入到队列中的某个尾节点,所以需要CAS。
2,在删除头节点的时候,不需要采用CAS,因为头节点的线程就只有一个,不存在竞争。
ConditionObject的数据结构-单向链表:
实现原理:
ConditionObject内部实现了一个FIFO(先进先出)等待队列,队列的每个节点都是等待在Condition对象上的线程 的引用,在调用Condition的await()方法之后,线程释放锁,构造成相应的节点进入等待队列等待,其中节点的定义复用AQS的Node定义。插入节点只需要将原有尾节点 的nextWaiter指向当前节点,并且更新尾节点。更新节点并没有像AQS更新同步队列使用CAS是因为调用await()方法的线程必定是获取了锁的线程,锁保证了操作的线程安全。当调用await方法之后,会将同步队列中的线程放入到等待队列中,当调用single唤醒之后,处于就绪状态,会将等待队列中的线程放入到同步队列中。
为什么不建议用singleAll唤醒?
因为singleAll会将所有的线程都唤醒,然后全部加入到同步队列中,而同步队列中只有一个线程可以获取到锁,所以对于这种操作是无用功。
注意:AQS实质上拥有一个同步队列和多个等待队列。
13.阻塞队列:
1,概念、生产者消费者模式:
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。 1)当队列满的时候,插入元素的线程被阻塞,直达队列不满。 2)队列为空的时候,获取元素的线程被阻塞,直到队列不空。
2,生产者和消费者模式:
生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很 慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。生产者和消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
3,常用方法:
方法 抛出异常 返回值 一直阻塞 超时退出
插入方法 add offer put Offer(time)
移除方法 remove poll take Poll(time)
检查方法 element peek N/A N/A
1,抛出异常:当队列满时,如果再往队列里插入元素,会抛 出IllegalStateException(”Queuefull”)异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
2,返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。如果是移除方法,则是从队列里取出一个元素,如果 没有则返回null。 3,一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。 4,超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。
4,常用阻塞队列:
1,ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列,按照先进先出原则,要求设定初始大小。
2,LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列,按照先进先出原则,可以不设定初始大小,默认是Integer.Max_Value。
3,ArrayBlockingQueue和LinkedBlockingQueue不同:
锁上面:ArrayBlockingQueue只有一个锁,LinkedBlockingQueue用了两个锁。
实现上:ArrayBlockingQueue直接插入元素,LinkedBlockingQueue需要转换。
4,PriorityBlockingQueue:
一个支持优先级排序的无界阻塞队列,默认情况下,按照自然顺序,要么实现compareTo()方法,指 定构造参数Comparator。
5,DelayQueue:
一个使用优 先级队列实现的无界阻塞队列,支持延时获取的元素的阻塞队列,元素必须要实现Delayed接口。
适用场景:实现自己的缓存系统,订单到期,限时支付等等。
6,SynchronousQueue:
一个不存储元素的阻塞队列,每一个put操作都要等待一个take操作,所以会产生生产者和消费者能力不匹配的问题。
7,LinkedTransferQueue:
一个由链表结构组成的无界阻塞队列,transfer(),必须要消费者消费了以后方法才会返回,tryTransfer()无论消费者是否接 收,方法都立即返回。
8,LinkedBlockingDeque:
一个由链表结构组成的双向阻塞队列,可以从队列的头和尾都可以插入和移除元素,实现工作密取,方法名带了First对头部操作,带了last从尾部操作,另外:add=addLast; remove=removeFirst; take=takeFirst。
14.线程池:
当并发线程多了之后,每个线程的频繁创建于销毁会导致性能降低,不好管理,为了解决这样的问题,就出现了线程池,线程池就是管理线程,我们不需要关心的创建与 销毁,大大增加了我们开发的效率和程序的性能。
作用:
1、 降低资源的消耗。降低线程创建和销毁的资源消耗。
2、 提高响应速度:线程的创建时间为T1,执行时间T2,销毁时间T3,免去T1和T3的时间。
3、 提高线程的可管理性。
线程池的创建:
ThreadPoolExecutor,jdk所有线程池实现的父类。
其中各个参数含义:
int corePoolSize:线程池中核心线程数小于corePoolSize(线程池大小),就会创建新线程去执行任务。等于corePoolSize,这个任务就会保存到BlockingQueue。 如果调用prestartAllCoreThreads方法就会一次性的启动corePoolSize个数的线程。
int maximumPoolSize:允许的大线程数,当BlockingQueue也满了,并且小于maximumPoolSize时候就会再次创建新的线程。
keepAliveTime:线程空闲下来后存活的时间,这个参数只在大于corePoolSize才有用。
TimeUnit unit:存活时间的单位值。
BlockingQueue[HTML_REMOVED] workQueue:保存任务的阻塞队列。
ThreadFactory threadFactory;创建线程的工厂,给新建的线程赋予名字.
RejectedExecutionHandler handler(饱和策略,四种):
1,AbortPolicy :直接抛出异常,默认。
2,CallerRunsPolicy:用调用者所在的线程来执行任务,不用线程池执行了。
3,DiscardOldestPolicy:丢弃阻塞队列里老的任务,队列里靠前的任务。
4,DiscardPolicy :当前任务直接丢弃。
饱和策略:
即是当线程数大于maximumPoolSize的时候,已经饱和的了,提供一个这四种处理的策略。 实现自己的饱和策略,需要实现RejectedExecutionHandler接口。
工作机制:
1)线程池判断核心线程池里的线程是否全都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。
2)线程池判断任务阻塞队列是否已经满。如果任务阻塞队列没有满,则将新提交的任务存储在这个任务阻塞队列里。如果任务阻塞队列满了,则进入下个流程。
3)线程池判断线程池中的工作的线程数是否小于大线程池数。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
方法:
提交任务:
execute(Runnable command) :不需要返回
Future[HTML_REMOVED] submit(Callable[HTML_REMOVED] task) :需要返回
关闭线程池:
shutdown(),shutdownNow();
shutdownNow():设置线程池的状态,还会尝试停止正在执行和没有执行任务的线程。
shutdown():设置线程池的状态,只会中断所有没有执行任务的线程。
合理配置线程池:
根据任务的性质区分:
计算密集型(CPU密集型),IO密集型,混合型
计算密集型:加密,大数分解,正则…….,这种类型,在使用线程池的时候,设置线程数适当小一点,大推荐:机器的Cpu核心数+1。(注意:这是逻辑核心数)
1,为什么+1,?
为了防止页缺失(页缺失:数据未全部从磁盘读取到内 存中,数据丢失了),(机器的Cpu核心=Runtime.getRuntime().availableProcessors();)
2,为什么不是大于CPU核心数+1? 因为如果线程数大于太多CPU核心数,就会出现不必要的上下文切换,造成性能消耗。 IO密集型:读取文件,数据库连接,网络通讯, 线程数适当大一点,机器的Cpu核心数*2,
混合型:尽量拆分,IO密集型>>计算密集型,拆分意义不大,IO密集型~计算密集型
注意:队列的选择上,应该尽量使用有界,无界队列可能会导致内存溢出,发生OOM。
Executor框架:
1,一个接口,其定义了一个接收Runnable对象的方法executor,其方法签名为executor,是线程池的定级接口,ThreadPoolExecutor是它的实现类。
2,ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,以及可跟踪一个或多个异步任务执行状况返 回Future的方法。
3,可以通过Executors这个工厂类获取到一些其他的预定义线程池,这些线程池都是Executor的实现类。
预定义的线程池:
FixedThreadPool:
创建固定线程数量的,适用于负载较重的服务器,其中使用了无界的阻塞队列(LinkedBliockingQueue)。
SingleThreadExecutor:
创建单个线程,可以让任务按顺序执行,不会有多个线程活动,使用了无界队列。
CachedThreadPool:
会根据需要来创建新线程的,执行很多短期异步任务的程序,使用了SynchronousQueue。
WorkStealingPool(JDK7以后):
基于ForkJoinPool实现,是一个工作密取的线程池。
ScheduledThreadPoolExecutor:
需要定期执行周期任务,Timer不建议使用了。
newSingleThreadScheduledExecutor: 只包含一个线程,只需要单个线程执行周期任务,保证顺序的执行各个任务。
newScheduledThreadPool:
可以包含多个线程的,线程执行周期任务,适度控制后台线程数量的时候。
方法说明:
schedule:只执行一次,任务还可以延时执行
scheduleAtFixedRate:提交固定时间间隔的任务,间隔是上一次开始的时刻到下一次开始的时刻的间隔。
scheduleAtFixedRate任务超时:
假设:规定60s执行一次,第一个任务 时长 80s,第二个任务20s,第三个任务50s,则: 第一个任务第0秒开始,第80S结束;
第二个任务第80s开始,在第100秒结束;
第三个任务第120s秒开始,170秒结束;
第四个任务从180s开始;
scheduleWithFixedDelay:
提交固定延时间隔执行的任务,间隔是上一次完成的时刻到下一次开始执行的时刻的间隔。
注意:当任务执行异常,如果直接抛出,会阻塞住当前任务,其他任务不会影响,所以,尽量用try,catch处理整个任务代码块,防止任务异常的时候阻塞。
CompletionService:
问题:当线程池中的任务是有返回值的时候,放入线程池的任务是有顺序的,所以当这些任务执行完成之后,从阻塞队列中拿执 行的任务的结果的时候,也只能是按照放入的顺序,先进先出的规则拿去结果,这样可能会造成CPU的浪费,比如我只想拿第三个任务执行的结果,这时候我必须得等到前两个任 务执行完才能拿到,同时,也不能让先完成的任务先去执行下一个任务,所以这样由此产生了CompletionService来解决。
CompletionService的实现是维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这 个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象 加入到Queue中。
15.并发安全:
类的线程安全定义:
如果多线程下使用这个类,不管多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的。 类的线程安全表现为:
1,操作的原子性
2,内存的可见性
不做正确的同步,在多个线程之间共享状态的时候,就会出现线程不安全。
怎么才能做到类的线程安全?
1,栈封闭:
所有的变量都是在方法内部声明的,这些变量都处于栈封闭状态,都是线程安全的。
2,无状态:
没有任何成员变量的类,就叫无状态的类。
3,让类不可变:
让状态不可变,两种方式:
1,加final关键字,对于一个类,所有的成员变量应该是私有的,同样的只要有可能,所有的成员变量应该加 上final关键字,但是加上final,要注意如果成员变量又是一个对象时,这个对象所对应的类也要是不可变,才能保证整个类是不可变的。
2、根本就不 提供任何可供修改成员变量的地方,同时成员变量也不作为方法的返回值。
4,volatile:
保证类的可见性,适合一个线程写,多个线程读的情景。
5,加锁和CAS。
6,安全的发布:
类中持有的成员变量,特别是对象的引用,如果这个成员对象不是线程安全的,通过get等方法发布出去,会造成这个成员对象本身持有的数据在多线程下不正确 的修改,从而造成整个类线程不安全的问题。
Servlet:
不是线程安全的类,为什么我们平时没感觉到:
1、在需求上,很少有共享的需求。
2,接收到了请求,返回应答的时候,都是由一个线程来负责的。
线程安全问题: 死锁: 资源一定是多于1个,同时小于等于竞争的线程数,资源只有一个,只会产生激烈的竞争。 死锁的根本成因:获取锁的顺序不一致导致。
产生死锁的原因:
1,因为系统资源不足
2,进程运行推进的顺序不合适
3,资源分配不当等
产生死锁的四个必要条件:
1,互斥条件:一个资源每次只能被一个进程使用
2,请求与保持条件:一个进程因请求字眼而阻塞时,对已获得的资源保持不放
3,不可剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
4,循环等待条件:若干进程之间形成一种头尾相接的循环等到资源关系
怀疑发送死锁:
简单的死锁:
1,通过jps 查询应用的 id,
2,再通过jstack id 查看应用的锁的持有情况
解决办法:保证加锁的顺序性
动态的死锁: 动态顺序死锁,在实现时按照某种顺序加锁了,但是因为外部调用的问题,导致无法保证加锁顺序而产生的。 解决:
1、 通过内在排序,保证加锁的顺序性
2、 通过尝试拿锁,也可以。
活锁:
尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生拿锁,释放锁的过程。
解决办法:每个线程休眠随机数,错开拿锁的时间。
线程饥饿:
低优先级的线程,总是拿不到执行时间。
性能和思考:
使用并发的目标是为了提高性能,引入多线程后,其实会引入额外的开销,如线程之间的协调、增加的上下文切换,线程的创建和销毁,线程的调度等等。过度的 使用和不恰当的使用,会导致多线程程序甚至比单线程还要低。 衡量应用的程序的性能:服务时间,延迟时间,吞吐量,可伸缩性等等,其中服务时间,延迟时间(多 快),吞吐量(处理能力的指标,完成工作的多少)。多快和多少,完全独立,甚至是相互矛盾的。 对服务器应用来说:多少(可伸缩性,吞吐量)这个方面比多快 更受重视。
我们做应用的时候:
1、 先保证程序正确,确实达不到要求的时候,再提高速度。(黄金原则)
2、 一定要以测试为基准。
一个应用程序里,串行的部分是永远都有的。
Amdahl定律 :
1/(F+(1-N)/N) F:必须被串行部分,程序好的结果, 1/F。
影响性能的因素:
1,上下文切换:
是指CPU 从一个进程或线程切换到另一个进程或线程。一次上下文切换花费5000~10000个时钟周期,几微秒。在上下文切换过程中,CPU会停 止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本 书当前读到的页码。 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗大的操作。
2,内存同步: 一般指加锁,对加锁来说,需要增加额外的指令,这些指令都需要刷新缓存等等操作。
3,阻塞: 会导致线程挂起【挂起:挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在 资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态 即就绪态,系统在超过一定的时间没有任何动作】。很明显这个操作包括两次额外的上下文切换。
优化性能:
1,减少锁的竞争
2,减少锁的粒度:
使用锁的时候,锁所保护的对象是多个,当这些多个对象其实是独立变化的时候,不如用多个锁来一一保护这些对象。但是如果有同时要持有多个 锁的业务方法,要注意避免发生死锁缩小锁的范围 对锁的持有实现快进快出,尽量缩短持由锁的的时间。将一些与锁无关的代码移出锁的范围,特别是一些耗时,可能阻塞的操作
3,避免多余的缩减锁的范围: 两次加锁之间的语句非常简单,导致加锁的时间比执行这些语句还长,这个时候应该进行锁粗化—扩大锁的范围。
4,锁分段:ConcurrrentHashMap就是典型的锁分段。
5,替换独占锁:
在业务允许的情况下:
1、 使用读写锁,
2、 用自旋CAS
3、 使用系统的并发容器
小结
对于并发问题,其实在我们项目中很常见,所以我们先了解线程与线程池,再是高并发,还有并发应用与高级工程,本次参考《java编程思想》与《head first》,其实这一次分享有这样的思路,就是我们了解了多线程,但是不安全,为什么线程安全我们就会想到同步(代码块,函数体),同步就要用到基本的锁,我们一般常用排它锁(Java里面的锁比较多,建议系统学习),然后我们有去想如何提高线程的效率,这样我们就来到了线程池(创建什么种类的线程池?),了解一些常见问题,例如Executor框架和线程如何合理分配等问题,最后才会到高并发问题,例如双11怎么去解决怎么大访问量?这就用到了高并发等知识点!当然这些都牵涉了太多的知识点,好了这就使并发的基础知识点!由于篇幅过于长,所以有错误私信我!谢谢你的阅读,希望你有所收获!
太棒了
“阻塞: 会导致线程挂起【挂起:挂起进程在操作系统中可以定义”
这里是挂起进程or线程??
原文没有错误,其实这里你可以区分一下进程与线程,再带入去理解Amdahl定律的影响因素!这里我可以建议去看一个线程状态转换图!
太棒了!
太棒了!
666,作为一个会一点点java的人完全看不懂(惨案
谦虚了 hh