concurrent包概述
concurrent包结构
concurrent包类图结构
concurrent包综述
- 综述: 在整个并发包设计上,Doug Lea大师采用了3.1 Concurrent包整体架构的三层结构
- 补充: 并发包所涉及的内容笔者会陆续推出对应番进行阐述,敬请期待(进度视笔者的忙碌程度而定)
1. 底层-硬件指令支持
- 综述: 并发包最底层是依赖于硬件级别的Volatile和CAS的支持
- Volatile:借用 Volatile 的内存读写语义和阻止重排序保证数据可见性
- CAS: 借用CAS的高效机器级别原子指令保证内存执行的 读-改-写 操作的原子性
- 组合: 借用 Volatile 变量的读/写和CAS实现线程之间的有效通信,保证了原子性、可见性、有序性
2. 中间层-基础数据结构+算法支持
- 综述: 在数据结构和算法的设计使用上,Doug Lea大师专门设计了AQS框架作为所有并发类库的并发基础,同时引入非阻塞算法和原子变量类增强了并发特性
- AQS框架: AQS中提供了最基本、有效的并发API, Doug Lea大师期望其作为所有并发操作的基础解决方案,并发包中的绝大部分实现都是依赖于AQS(AbstractQueuedSynchronizer),同时 AQS的基础是 CAS 和 Volatile的底层支持
- 非阻塞数据结构: 非阻塞数据结构是非阻塞队列的设计基础,同时也是阻塞队列的参考对比的重要依据
- 原子变量类: Doug Lea大师专门为所有的原子变量设计了专门的类库,甚至在后期还对齐做了增强,比如 LongAdder、LongAccumulator 等,从侧面可以反映出数值操作对于编程的重要性
3. 高层-并发类库支持
- 综述: Doug Lea大师在并发包中已经提供了丰富的并发类库极大方便了快速、安全的使用并发操作
- Lock: Lock接口定义了一系列并发操作标准,详情参见 AQS框架之Lock
- 同步器: 每个并发类的同步器的实现依赖于AQS(继承),比如 ReentrantLock 中的Sync;同时笔者也将 并发类 同属于同步器的范围内
- 阻塞队列: 顾名思义,支持阻塞的队列,主要是以Queue结尾的类
- 执行器: 所谓执行器,指的是任务的执行者,比如线程池和Fork-Join
- 并发容器: 即支持并发的容器,主要包含COW和以Concurrent开头的类,通常并发容器是非阻塞的
Lock接口
synchronized的不足
- 不可中断:使用内部锁(指的是 synchronized) 时,不能中断正在等待获取锁的线程
- 不可超时:使用内部锁时,在请求锁失败情况下,必须无限等待,没有超时效果
- 自动释放:使用内部锁时,内部锁必须在获取它们的代码块中被自动释放(虽然对代码来说是种简化且对异常友好)
- 不可伸缩:使用内部锁时,无法细粒度控制锁(伸缩性不足),即无法实现锁分离和锁联结,比如为每个链表节点(或部分)加锁从而允许不同的线程能够独立操作链表的不同节点(部分),遍历或修改链表时,需先获取该节点锁并直到获取下一个节点锁时才释放当前节点锁
- 性能问题:使用内部锁时,在有竞争情况下仍会出现性能问题,尽管JDK6对内部锁进行了优化,但无论是偏向锁或是轻量级锁都是针对无竞争情况的优化,无竞争情况下与 ReentractLock 性能一致,但有竞争时Lock明显更高效
Lock接口综述
- 定义: JDK1.5 引入Lock接口,其定义了一些抽象的锁操作,相比synchronized,Lock 提供了无条件、可轮询、可定时、可中断的锁获取操作,所有加锁和解锁的方法都是显式的
- 实现: Lock 的实现必须提供具有与 synchronized 相同的内存语义,但加锁的语义、调度算法、顺序保证、性能特性可以有所不同
- 使用: Lock接口的实现基本是通过聚合一个同步器 AbstractQueuedSynchronized 的子类来完成线程的访问控制
- 对比内部锁: Lock缺少隐式获取/释放锁的便捷,但却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种内部锁不具备的同步性,甚至还支持读写锁分离,同时允许获取和释放可以不在同一个块中
- lock接口对应实现在concurrent包下的locks包内,结构如下图:
lock接口优势:
lock接口方法
lock()
- lock方法应具有与内部锁加锁相同的内存语义,即无锁阻塞和支持可重入
- lock方法必须搭配unlock方法使用,同时必须在finally中显式调用unlock方法释放锁
/**
* Acquires the lock.
* 获取锁,调用该方法的当前线程将会获取锁,当锁获得后,从该方法返回
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until the
* lock has been acquired.
* 若当前锁不可用(已被占有),当前线程会一直休眠直到锁为可被获取状态
* <p><b>Implementation Considerations</b>
* 实现该方法的注意事项
* <p>A {@code Lock} implementation may be able to detect erroneous use
* of the lock, such as an invocation that would cause deadlock, and
* may throw an (unchecked) exception in such circumstances. The
* circumstances and the exception type must be documented by that
* {@code Lock} implementation.
* 该方法的实现需要能发现lock被错误使用,如死锁或抛出不可查异常(即可运行期异常和Error)
* 此时该实现必须用文档注明其可能出现的异常或需要的使用环境
*/
void lock();
lockInterruptibly()
- lockInterruptibly 方法提供可中断的锁获取操作并允许在可取消的活动中使用
说明:
/**
* Acquires the lock unless the current thread is
* {@linkplain Thread#interrupt interrupted}.
* 可中断地获取锁,即在锁的获取中可以中断当前线程
* <p>Acquires the lock if it is available and returns immediately.
* 当获取锁时锁可用就立即返回
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until
* one of two things happens:
* <ul>
* <li>The lock is acquired by the current thread; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of lock acquisition is supported.
* </ul>
* 若当前锁不可用(已被占有),当前线程会一直休眠直到以下两种情况发生:
* 1.锁被当前线程获取
* 2.其他线程中断当前线程,同时锁的获取允许被中断
* <p><b>Implementation Considerations</b>
* 实现该方法的注意事项
* <p>The ability to interrupt a lock acquisition in some
* implementations may not be possible, and if possible may be an
* expensive operation. The programmer should be aware that this
* may be the case. An implementation should document when this is
* the case.
* 该方法属于拓展方法,只有需要中断服务的时候才需要实现它
* <p>An implementation can favor responding to an interrupt over
* normal method return.
* 相对于返回,该方法更适合抛出一个中断响应,比如中断异常
* @throws InterruptedException if the current thread is
* interrupted while acquiring the lock (and interruption
* of lock acquisition is supported)
*/
void lockInterruptibly() throws InterruptedException;
//样例代码
public boolean doTask throws InterruptedException(){
lock.lockInterruptibly();
try{
return cancelTask();
}finally{
lock.unlock();
}
}
//取消任务
private boolean cancelTask() throws InterruptedException {...}
tryLock()
- tryLock 方法提供可定时与可轮询的锁获取方式,与无条件的锁获取相比,具有更完善的错误恢复机制
- tryLock 方法能够有效的防止死锁的发生,比如使用轮询锁优雅失败规避死锁
- tryLock 方法同时提供定时锁的功能,其允许在限时活动内部使用独占锁,当线程获取锁、被中断或超时后返回
- tryLock 方法支持轮询获取锁:通过一个循环配合tryLock()来不断尝试获取锁,由于tryLock()非阻塞因此会立即返回是否成功获取锁的结果;当不能获取所有的锁时,应释放已获得的所有锁并重新尝试获取
- tryLock 方法同时支持响应中断
/**
* Acquires the lock only if it is free at the time of invocation.
* 尝试非阻塞的获取锁,调用该方法后立即返回是否成功获取锁true/false
* <p>Acquires the lock if it is available and returns immediately
* with the value {@code true}.
* If the lock is not available then this method will return
* immediately with the value {@code false}.
* 当锁不可用时立即返回false
* This usage ensures that the lock is unlocked if it was acquired, and
* doesn't try to unlock if the lock was not acquired.
* 该实现应确保当锁被获取时是未锁状态,当未被获取时不会尝试解锁
* @return {@code true} if the lock was acquired and
* {@code false} otherwise
*/
boolean tryLock();
/**
* Acquires the lock if it is free within the given waiting time and the
* current thread has not been {@linkplain Thread#interrupt interrupted}.
* 没有被中断当前线程在指定超时时间内获取锁
* If the lock is not available then the current thread becomes disabled for
* thread scheduling purposes and lies dormant until one of three things happens:
* <ul>
* <li>The lock is acquired by the current thread; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of lock acquisition is supported; or
* <li>The specified waiting time elapses
* </ul>
* <p>If the specified waiting time elapses then the value {@code false} is returned.
* If the time is less than or equal to zero, the method will not wait at all.
* 当前线程在以下三种情况下会返回:
* 1.当前线程在超时时间内获得锁
* 2.当前线程在超时时间内被中断
* 3.超时时间结束,返回false,线程不再被阻塞
* @param time the maximum time to wait for the lock
* @param unit the time unit of the {@code time} argument
* @return {@code true} if the lock was acquired and {@code false}
* if the waiting time elapsed before the lock was acquired
* @throws InterruptedException if the current thread is interrupted
* while acquiring the lock (and interruption of lock
* acquisition is supported)
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
CAS
CAS,compare and swap的缩写,中文翻译成比较并交换
在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
CAS原理
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。
整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。而compareAndSwapInt就是借助C来调用CPU底层指令实现的。
下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x)
1、当前的实例 2、实例变量的内存地址偏移量 3、预期的旧值 4、要更新的值
首先说明,处理器会自动保证基本的内存操作是原子性的。处理器保证从系统内存中读取或写入一个字节是原子的。意思是,当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
当然, long和 double类型在32位操作系统中的读写操作不是原子的,因为 long和 double占64位,需要分成2个步骤来处理,在读写时分别拆成2个字节进行读写。因此 long和 double类型的数据在进行计算时需要注意这个问题。
总线锁
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。如下图
处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。
缓存锁
第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效,在例1中,当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
ABA问题
假设这样一种场景,当第一个线程执行CAS(V,E,U)操作。在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:
这就是典型的CAS的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下原子类
AtomicStampedReference类
AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境
底层实现为: 通过Pair私有内部类存储数据和时间戳, 并构造volatile修饰的私有实例。
用CAS实现volatile原子性方式
原子性表现为每个可以单独操作,不互相依赖,在线程中表现为每个线程都有所以它自己的一份copy值,不定期的刷新到主内存。(如果有锁,ulock时刷新到主内存)
而volatile变量不具有原子性,每次读写都是自己去主内存读主内存的值,也真是由于此种原因不能进行计数器操作,例如:
volatile i =1;
线程A,线程B 同时 i++;
i++ 即
i=i; //从主内存中读 1
i+1; //通过获取的值。计算 2
i=i+1; //把计算的值写入主内存中 3
当线程执行顺序如下时 A1 – >B1—>A2—>A3—>A1—>B2—>B3, 最后结果导致运行了两次结果还是2
对此,可以用CAS算法进行改进,CAS也可成为乐观锁,实现原理,通过保存原有值进行比较结果,直到更改成功,即自旋volatile变量实现。
实现原理,CAS保存了3个值 H当前值(作为预期值),V内存值,S计算值
代码实现如下:
public final int incrementAndGet(h, s) {
for (;;) {
inth=i; //A线程叫AH,B线程描述为BH 1
int s = i +1; // A线程叫AS,B线程描述为BS 2
if(h==i){ // 比较内存值和预期值 3
i=s; // 如果相同,赋值,成功CAS 4
return s;
}
}
A1 (A开始时用AH保存内存中此时的i值)->
B1(B开始时也用BH保存当前i值)->
A2 (把计算值2赋给AS)
A3(比较保存的AH和读取内存值Ai,都是等于1,未改变)
A4(所以CAS成功,把AS即2放入内存中)
B2(把计算值2赋给BS)
B3(比较BH和读取当前内存值Bi,BH是1,Bi是2,所以不相等,返回到B1)
B1 (故重新取出内存值i,重复计算,此时BH=Bi=2,BS=3赋给主内存,完成计数)
CAS特点:
优点:
- 竞争不大的时候系统开销小。
缺点:
-
循环时间长开销大。
-
ABA问题。
-
只能保证一个共享变量的原子操作。
AQS
概述
java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷性的隐式获取锁释放锁机制(基于JVM机制),但是它却缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。
在介绍Lock的时候,我们需要先熟悉一个非常重要的组件,掌握了该组件JUC包下面很多问题都不在是问题了。该组件就是AQS。
AQS:AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。
AQS解决了子啊实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
在基于AQS构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计AQS时充分考虑了可伸缩行,因此J.U.C中所有基于AQS构建的同步器均可以获得这个优势。
AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。当state>0时表示已经获取了锁,当state = 0时表示释放了锁。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:
- int getState():返回同步状态的当前值。
- void setState(long newState):设置同步状态的值。
- boolean compareAndSetState(int expect, int update):如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。
AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
基本方法
- getState():返回同步状态的当前值;
- setState(int newState):设置当前同步状态;
- compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性;
- tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态
- tryRelease(int arg):独占式释放同步状态;
- tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;
- tryReleaseShared(int arg):共享式释放同步状态;
- isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;
- acquire(int arg):独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;
- acquireInterruptibly(int arg):与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
- tryAcquireNanos(int arg,long nanos):超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;
- acquireShared(int arg):共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
- acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断;
- tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制;
- release(int arg):独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;
- releaseShared(int arg):共享式释放同步状态;
独占锁和非独占锁
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
acquire(int):
此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程:
- tryAcquire()尝试直接去获取资源,如果成功则直接返回;
- 如果失败,addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
- acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
- 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
tryAcquire()方法
此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。AQS这里只定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。
addWaiter()方法
此方法用于将当前线程加入到等待队列的队尾,并返回当前线程所在的结点。
private Node addWaiter(Node mode) {
// 1. 将当前线程构建成Node类型
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 2. 当前尾节点是否为null?
Node pred = tail;
if (pred != null) {
// 2.2 将当前节点尾插入的方式插入同步队列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程
enq(node);
return node;
}
程序的逻辑主要分为两个部分:
- 当前同步队列的尾节点为null,调用方法enq()插入;
- 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。
另外还会有另外一个问题:如果 if (compareAndSetTail(pred, node))
为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环)进行重试。
enq(final Node node)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//1. 构造头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 2. 尾插入,CAS操作失败自旋尝试
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
对enq()方法可以做这样的总结:
- 在当前线程是第一个加入同步队列时,调用compareAndSetHead(new Node())方法,完成链式队列的头结点的初始化;
- 自旋不断尝试CAS尾插入节点直至成功为止。
acquireQueued()
在同步队列中的节点(线程)会做什么事情了来保证自己能够有机会获得独占式锁了?带着这样的问题我们就来看看acquireQueued()方法,从方法名就可以很清楚,这个方法的作用就是排队获取锁的过程:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 1. 获得当前节点的先驱节点
final Node p = node.predecessor();
// 2. 当前节点能否获取独占式锁
// 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
if (p == head && tryAcquire(arg)) {
//队列头指针用指向当前节点
setHead(node);
//释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;
}
// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
程序逻辑通过注释已经标出,整体来看这是一个这又是一个自旋的过程(for (;;)),代码首先获取当前节点的先驱节点,如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁。反之,获取锁失败进入等待状态。
release()方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这段代码逻辑就比较容易理解了,如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。unparkSuccessor方法源码:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//头节点的后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//后继节点不为null时唤醒该线程
LockSupport.unpark(s.thread);
}
首先获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。
acquireInterruptibly()方法
我们知道lock相较于synchronized有一些更方便的特性,比如能响应中断以及超时等待等特性,现在我们依旧采用通过学习源码的方式来看看能够响应中断是怎么实现的。可响应中断式锁可调用方法lock.lockInterruptibly();而该方法其底层会调用AQS的acquireInterruptibly方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
//线程获取锁失败
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//将节点插入到同步队列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//获取锁出队
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//线程中断抛异常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
与acquire方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。
tryAcquireNanos()方法(超时等待获取锁)
通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:
- 在超时时间内,当前线程成功获取了锁;
- 当前线程在超时时间内被中断;
- 超时时间结束,仍未获得锁返回false。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
//实现超时等待的效果
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//1. 根据超时时间和当前时间计算出截止时间
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//2. 当前线程获得锁出队列
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 3.1 重新计算超时时间
nanosTimeout = deadline - System.nanoTime();
// 3.2 已经超时返回false
if (nanosTimeout <= 0L)
return false;
// 3.3 线程阻塞等待
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 3.4 线程被中断抛出被中断异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
显然这段源码最终是靠doAcquireNanos方法实现超时等待的效果。
程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上,在第1步会先计算出按照现在时间和超时时间计算出理论上的截止时间,比如当前时间是8h10min,超时时间是10min,那么根据deadline = System.nanoTime() + nanosTimeout
计算出刚好达到超时时间时的系统时间就是8h 10min+10min = 8h 20min。然后根据deadline - System.nanoTime()
就可以判断是否已经超时了,比如,当前系统时间是8h 30min很明显已经超过了理论上的系统时间8h 20min,deadline - System.nanoTime()
计算出来就是一个负数,自然而然会在3.2步中的If判断之间返回false。如果还没有超时即3.2步中的if判断为true时就会继续执行3.3步通过LockSupport.parkNanos使得当前线程阻塞,同时在3.4步增加了对中断的检测,若检测出被中断直接抛出被中断异常。流程如下图:
acquireShared()方法(共享锁的获取)
共享锁的获取如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
这段源码的逻辑很容易理解,在该方法中会首先调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则,表明获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared方法
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 当该节点的前驱节点是头结点且成功获取同步状态
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
逻辑几乎和独占式锁的获取一模一样,这里的自旋过程中能够退出的条件是当前节点的前驱节点是头结点并且tryAcquireShared(arg)返回值大于等于0即能成功获得同步状态。
releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
这段方法跟独占式锁释放过程有点点不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态,这里采用的CAS保证,当CAS操作失败continue,在下一次循环中进行重试。
可中断(acquireSharedInterruptibly()方法),超时等待(tryAcquireSharedNanos()方法)
中断锁以及超时等待的特性其实现和独占式锁可中断获取锁以及超时等待的实现几乎一致,具体的就不再说了。
ReentrantLock
公平锁和非公平锁
非公平锁和公平锁的区别是在tryAccquire时,是否判断当前节点是头节点,如果是非公平锁不判断,直接CAS,先抢到先得。
如果是公平锁,必须是头节点才能CAS。
如何创建:
//默认非公平锁
Lock nonFairLock = new ReentrantLock();
nonFairLock.lock();
//创建公平锁
Lock fairLock = new ReentrantLock(true);
fairLock.lock();
Synchronized 和 ReenTrantLock 的对比
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
- ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的
ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 - synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
④ 性能已不是选择标准
在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。
ReentrantReadWriteLock
ReentrantLock(排他锁)具有完全互斥排他的效果,即同一时刻只允许一个线程访问,这样做虽然虽然保证了实例变量的线程安全性,但效率非常低下。ReadWriteLock接口的实现类-ReentrantReadWriteLock读写锁就是为了解决这个问题。
读写锁维护了两个锁,一个是读操作相关的锁也成为共享锁,一个是写操作相关的锁 也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。
多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的。)。
ReentrantReadWriteLock具有如下特性:
一、锁的获取顺序:
- 非公平模式(默认):当使用此种模式的时候,将不会指定进入读写锁的顺序,即一个线程获取到锁并释放后,可能立即再次获取锁,也可能导致一个线程可能一直尝试抢锁,但是获取不到,其吞吐量通常要高于公平锁。
- 公平模式:当使用此种模式的时候,线程利用一个近似到达顺序的策略来争夺进入,等待时间最长的线程将最先获取到锁,该种模式会保证获取锁的时间顺序,但是吞吐量会有所牺牲。
二、可重入性:当持有读锁的线程获取后能再次获取同一把锁,写锁获取之后能够再次获取写锁,同时也能够获取读锁,但是反之则不可以,即持有读锁的线程,不可以再次获取写锁。
三、锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,但是反之不可以,即读锁不可以升级为写锁。
四、重入数:读取锁和写入锁的数量最大分别只能是65535(包括重入数)。
CONDITION
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。
condition中实现的方法:
单个等待通知机制:
public class UseSingleConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.start();
Thread.sleep(3000);
service.signal();
}
static public class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println(" await时间为" + System.currentTimeMillis());
condition.await();
System.out.println("这是condition.await()方法之后的语句,condition.signal()方法之后我才被执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() throws InterruptedException {
lock.lock();
try {
System.out.println("signal时间为" + System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("这是condition.signal()方法之后的语句");
} finally {
lock.unlock();
}
}
}
static public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.await();
}
}
}
在使用wait/notify实现等待通知机制的时候我们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差不多,必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。
使用Condition实现顺序执行:
public class ConditionSeqExec {
volatile private static int nextPrintWho = 1;
private static ReentrantLock lock = new ReentrantLock();
final private static Condition conditionA = lock.newCondition();
final private static Condition conditionB = lock.newCondition();
final private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
Thread threadA = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 1) {
conditionA.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadA " + (i + 1));
}
nextPrintWho = 2;
//通知conditionB实例的线程运行
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadB = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 2) {
conditionB.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadB " + (i + 1));
}
nextPrintWho = 3;
//通知conditionC实例的线程运行
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadC = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 3) {
conditionC.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadC " + (i + 1));
}
nextPrintWho = 1;
//通知conditionA实例的线程运行
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread[] aArray = new Thread[5];
Thread[] bArray = new Thread[5];
Thread[] cArray = new Thread[5];
for (int i = 0; i < 5; i++) {
aArray[i] = new Thread(threadA);
bArray[i] = new Thread(threadB);
cArray[i] = new Thread(threadC);
aArray[i].start();
bArray[i].start();
cArray[i].start();
}
}
}
CyclicBarrier
CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarrier很有用。它的功能与Thread中的join()非常的相似,不过它的功能会更加的强大。
CyclicBarrier的实现机制是依赖于ReentrantLock于Condition实现的,CyclicBarrier构造方法如下:
CyclicBarrier(int parties)
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
------------------------------------------------------------------------------
CyclicBarrier(int parties, Runnable barrierAction)
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
类结构如下:
方法包括:
int await()
在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
------------------------------------------------------------------------------
int await(long timeout, TimeUnit unit)
在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
------------------------------------------------------------------------------
int getNumberWaiting()
返回当前在屏障处等待的参与者数目。
------------------------------------------------------------------------------
int getParties()
返回要求启动此 barrier 的参与者数目。
------------------------------------------------------------------------------
boolean isBroken()
查询此屏障是否处于损坏状态。
------------------------------------------------------------------------------
void reset()
将屏障重置为其初始状态。
示例如下:
public class CyclicBarrierDemo {
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + ", 等待其他线程准备就绪");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
System.out.println("全部线程就绪,开始执行");
}
}
源码分析
构造方法:
public CyclicBarrier(int parties, Runnable barrierAction) {
//线程数目小与等于0,抛出异常
if (parties <= 0) {
throw new IllegalArgumentException();
}
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
可以指定关联该CyclicBarrier的线程数量,并且可以指定在所有线程都进入屏障后的执行动作,该执行动作由最后一个进行屏障的线程执行。
如果不指定Runnable对象,即不进行任何操作。(默认构造方法barrierAction传null)
await()方法:
ublic int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
//1、获取锁
lock.lock();
try {
//2、保存当前代
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//3、计数器自减
int index = --count;
//4、当计数器为0时,结束流程
if (index == 0) { // tripped
boolean ranAction = false;
try {
//5、获取结束时执行动作
final Runnable command = barrierCommand;
//如果动作不为空,执行
if (command != null)
command.run();
ranAction = true;
//6、重置当前代
nextGeneration();
return 0;
} finally {
//7、未执行任何动作,破坏掉栅栏
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
// 进行死循环,直到被破坏、打断、超时,结束循环
for (;;) {
try {
//8、如果未设置超时,当前线程进入Condition的等待队列
if (!timed)
trip.await();
//如果设置了超时时间,当前线程在超时时间之前,进入等待队列等待
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//9、如果出现打断异常,判断保存的代等于当前代并且屏障没有被损坏
if (g == generation && ! g.broken) {
//10、破坏掉栅栏
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
//11、如果保存的代被破坏,抛出异常
if (g.broken)
throw new BrokenBarrierException();
//12、如果保存的代不等于当前代,返回index
if (g != generation)
return index;
//13、如果设置了等待时间,并且等待时间小于0,破坏栅栏,并抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//14、释放锁资源
lock.unlock();
}
}
nextGeneration()为释放需要等待的所有队列,并重置当前代。代的概念这里我们说一下,它是CyclicBarrier中的一个内部类,它的作用更像是一个标志位的作用,其只有一个属性,broken,记录当前代释是否被破坏。nextGeneration()会在全部线程进入屏障后会被调用,即生成下一个代,使得全部线程又可以重新进入到栅栏中,从这里可以得知,CyclicBarrier的栅栏是可以多次复用的,而这个特性与另一个功能相似的类CountDownLatch有所不同。
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
breakBarrier()破坏当前代,内部方法比较简单,将当前代的破坏状态设置为true,并唤醒在当前Condition对象的等待队列中等待的全部线程。
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
整个流程如下:
- 调用await方法
- 获取锁资源,加锁,判断当前代是否被破坏,如果被破坏,抛出BrokenBarrierException
- 判断线程是否被打断,如果被打断,broken置为true,唤醒所有等待线程,抛出InterruptedException
- 计数器自减,重置当前代,返回0
- 如果计数器不为0,循环线程进入当前condition的等待队列,并挂起,等待唤醒
- 中断、破坏和超时会跳出循环
- 被唤醒后,如果保存的代和当前代不等,返回当前计数
- 释放锁
使用场景:CyclicBarrier可以用于多个线程执行任务,需要等待多个线程全部执行完毕后,才可以输出最终结果,其在多线程开发场景下,非常的常用。
CountDownLatch
CountDownLatch是一个同步辅助类,与CyclicBarrier功能相似,它允许一组线程互相等待,直到到达某个公共屏障点。
但是它与CyclicBarrier不同,具体表现:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
- 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
- CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
CountDownLatch使用示例:
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println("等待其他线程执行开始");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("全部线程执行完毕");
}
}
CountDownLatch源码实现
CountDownLatch的实现是基于AQS的同步队列,通过重写AQS的抽象方法,同时采用共享锁的获取和释放方式。
代码如下:
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
CountDownLatch的构造函数是构造一个用给定计数初始化的CountDownLatch,并且构造函数内完成了sync的初始化,并设置了AQS的state值。
Sync继承于AQS ,重写了其tryAcquireShared、tryReleaseShared这两个方法。
await()方法:
await()的实现是基于AQS的,调用了其acquireSharedInterruptibly()方法,是AQS的共享式获取同步状态,首先判断了线程是否被中断,如果是,抛出异常,否则,调用子类的模板方法的实现。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//调用模板方法子类的实现,即CountdownLatch的实现
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
doAcquireSharedInterruptibly()方法:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //挂起当前线程
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
- 增加一个共享节点到同步队列尾部,如果当前队列中没有节点,创建一个假的头结点,将新的节点的前驱节点指向该假头结点
- 进入自旋,获取当前节点的前驱节点
- 如果前驱节点是头结点,调用模板方法的实现获取同步状态
- 如果结果大于0,设置新的头结点,并释放掉当前节点,并跳出循环
- 否则,将当前线程挂起
- 过程中如果被打断,会抛出中断异常
countDown()方法:
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
// 获取状态
int c = getState();
// 如果状态值为0
if (c == 0)
return false;
//否则,将状态值,减一
int nextc = c-1;
//CAS赋值
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
countDown()方法同样是基于AQS的方法进行实现,调用其releaseShared(),共享式获取同步状态方法,从tryReleaseShared方法我们可以看到,每次一个线程调用其countdown的时候,都会对state进行减一操作,直到state为0的时候,该方法返回true。
await具体流程:
- 线程执行await方法,进入同步队列;
- 新增节点,判断头节点是否为null,没有创建假的头节点(第一次)
- 加入同步队列尾部
- 自旋判断:前驱节点是否是头节点,如果是,释放。设置新的头节点,如果不是,当前节点线程挂起,等待唤醒;
cowndown方法流程:
- 调用countdown方法,线程调用tryReleaseShared()方法时,会将当前同步状态减一,当state为0的时候,调用doReleaseShared();
- 调用doReleaseShared做了一件事,唤醒头结点的线程;
- 头结点线程(第一个节点的头结点是假节点,没有持有线程,会唤醒其下一个真实节点的线程)被唤醒后,拿到同步资源,退出循环,并唤醒下一个节点的线程,依次类推,直到唤醒全部同步队列的线程.
Semaphore
Semaphore是一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。
Semaphore可以理解为一个流量控制器,只允许指定数目的线程拿到许可,继续执行,当有其他线程再希望拿到许可时,需要阻塞等待,直到拿到许可的线程执行完毕后,释放许可,其他线程才可以获取到许可,进行执行。
Semaphore 有两种模式,即公平模式与非公平模式,可以通过构造方法进行指定。
构造方法:
Semaphore(int permits)
创建具有给定的许可数和非公平的公平设置的 Semaphore。
---------------------------------------------------------------------
Semaphore(int permits, boolean fair)
创建具有给定的许可数和给定的公平设置的 Semaphore。
在非公平模式下,不对线程获取许可的顺序做任何保证,即一个线程可能刚刚获取完许可并释放许可,可以立刻再次获取许可。
在公平模式下,对于任何调用获取方法的线程而言,都按照处理它们调用这些方法的顺序(即先进先出;FIFO)来选择线程、获得许可。
几个重要的方法:
void acquire()
从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void release()
释放一个许可,将其返回给信号量。
void acquire(int permits)
从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
void release(int permits)
释放给定数目的许可,将其返回到信号量。
示例如下:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(1);
ExecutorService executors = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executors.execute(() -> {
System.out.println("线程池A,启动线程,当前线程名称:" + Thread.currentThread().getName());
try {
System.out.println("线程池A,当前线程名称:" + Thread.currentThread().getName() + ",准备尝试获取许可");
semaphore.acquire();
System.out.println("线程池A,当前线程名称:" + Thread.currentThread().getName() + ",获取许可成功");
Thread.sleep(5 * 1000);
} catch (Exception e) {
e.printStackTrace();
}
semaphore.release();
System.out.println("线程池A,当前线程名称:" + Thread.currentThread().getName() + "释放许可成功");
});
}
}
}
输出结果:
线程池A,启动线程,当前线程名称:pool-1-thread-2
线程池A,启动线程,当前线程名称:pool-1-thread-1
线程池A,当前线程名称:pool-1-thread-1,准备尝试获取许可
线程池A,启动线程,当前线程名称:pool-1-thread-3
线程池A,当前线程名称:pool-1-thread-1,获取许可成功
线程池A,当前线程名称:pool-1-thread-2,准备尝试获取许可
线程池A,当前线程名称:pool-1-thread-3,准备尝试获取许可
线程池A,当前线程名称:pool-1-thread-1释放许可成功
线程池A,当前线程名称:pool-1-thread-2,获取许可成功
线程池A,当前线程名称:pool-1-thread-2释放许可成功
线程池A,当前线程名称:pool-1-thread-3,获取许可成功
线程池A,当前线程名称:pool-1-thread-3释放许可成功
Semaphore的实现基本与另外几个并发辅助工具类差不多,内部实现一个同步器,去继承AQS,重写其模板方法,实现自己的功能。Semaphore支持两种模式,一种是公平模式,一种是非公平模式,这部分的实现也是基于AQS。具体源码分析略。
Semaphore、CountDownLatch、CyclicBarrier
Semaphore、CountDownLatch、CyclicBarrier这三个类都是用于实现并发辅助的工具类,但是它们在使用场景上有略微的区别,这里我们横向对比一下:
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限,而锁是控制对某个资源的访问权限。
参考文档:
https://www.cnblogs.com/romanjoy/p/8427960.html
https://blog.csdn.net/wtopps/article/details/82054186
https://blog.csdn.net/zhangdong2012/article/details/79983404
https://blog.csdn.net/m0_37822939/article/details/80040589
https://juejin.im/post/5aeb07ab6fb9a07ac36350c8#heading-6