JUC locks锁框架
早期的JDK版本中,仅仅提供了synchronizd
、wait
、notify
等等比较底层的多线程同步工具。java.util.concurrent.locks
包下提供了一系列基础的锁工具,用以对synchronizd
、wait
、notify
等进行补充、增强。
1 | interface Locks{ |
ReentrantLock类
ReentrantLock
类,实现了Lock
接口,是一种可重入的独占锁,它具有与使用 synchronized
相同的一些基本行为和语义,但功能更强大。ReentrantLock
内部通过内部类实现了AQS(AbstractQueuedSynchronizer
)框架的API来实现独占锁的功能。ReentrantLock
默认使用非公平的策略,但是可以通过构造器添加参数来使用公平策略。
公平策略按照先来后到的原则来允许线程获得锁,避免了饥饿,但是受到线程唤醒的影响效率并不如非公平策略。实际上在非公平模式下,并发线程会在尝试获取锁的时候会首先通过CAS修改state,如果失败,则仍然进入队列排队,按照先来后到的顺序获取锁。公平模式下则是直接进入队列。
ReentrantLock
中主要拥有一个内部类Sync
,这是AQS的子类,也是ReentrantLock
主要的实现原理。Sync
的实例有两种,一种是FairSync
另一种是NonfairSync
,分别对应公平策略和非公平策略。对锁的获取最终会委托给Sync.tryAcquire(int)
方法。这个方法是AQS中核心方法,留给子类实现。
在公平模式获取锁:
- 如果代表加锁次数的属性
state
为0,并且等待队列中没有其他线程,则以CAS的方式更新state
。然后设置当前线程获得锁。 - 如果状态不为0,且当前线程已经获得了锁,则说明是重入情况。此时更新
state
,并处理可能会出现的溢出异常。 - 如果不是前两种情况,则获取锁失败了。此时通过自旋,将当前线程包装成一个节点,插入到等待线程的双向链表中。
- 从等待队列中取出当前节点并判断是否是首节点,并尝试获取锁,如果获取不到,则需要根据前置节点的状态判断是否阻塞当前线程。为了保证当前线程能够被唤醒,前置节点必须为SIGNAL。如果为CANCELLED,则说明前置节点应该被移除,并重新尝试。最终设置前置节点状态为SIGNAL然后阻塞当前线程。
释放锁:
- 当前线程必须是锁的独占线程,然后将
state
减1。如果state
为0,则将独占线程设置为null
。 - 遍历队列,如果节点状态不为CANCELLED,则唤醒这个节点。
- 被唤醒节点会返回自己的中断标识,标识自己在阻塞期间是否被中断,这里是一种延迟中断策略。然后尝试获取锁。
- 获取锁之后会清楚其前置节点,并将自己设置为队首元素。
对于等待线程被中断时候,acquireQueued
方法会用一个标志位用来记录,并不会抛出异常,而doAcquireInterruptibly
方法则会直接抛出异常。
对于限时等待的支持,通过doAcquireNanos
方法实现。这个方法是一个自旋操作,在规定时间内不断尝试获取锁,获取不到就阻塞,并且会响应中断。其阻塞是通过Unsafe.park
方法调用native方法实现的。如果超时获取不到锁就会调用cancelAcquire
方法取消一个正在获取锁的线程。该方法主要将到期的节点等待状态修改为CANCELLED,然后等待被GC回收。
LockSupport类
LockSupport
类,是JUC包中的一个工具类,是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport
类的核心方法其实就只有两个Park
和Unpark
,一个用于阻塞一个用于唤醒线程,相比于wait
和signial
方法,其能针对特定线程进行操作。
AbstractQueuedSynchronizer抽象类
AQS是整个JUC包的核心,提供了一套通用的机制来管理同步状态(synchronization state)、阻塞/唤醒线程、管理等待队列。ReentrantLock
、CountDownLatch
、CyclicBarrier
等同步器,其实都是通过内部类实现了AQS框架暴露的API,以此实现各类同步器功能。这些同步器的主要区别其实就是对同步状态的定义不同。
AQS框架,分离了构建同步器时的一系列关注点,它的所有操作都围绕着资源——同步状态来展开,并替用户解决了如下问题:
- 资源是可以被同时访问?还是在同一时间只能被一个线程访问?(共享/独占功能)
- 访问资源的线程如何进行并发管理?(等待队列)
- 如果线程等不及资源了,如何从等待队列退出?(超时/中断)
这其实是一种典型的模板方法设计模式:父类(AQS框架)定义好骨架和内部操作细节,具体规则由子类去实现。
同步器 | 资源的定义 |
---|---|
ReentrantLock | 资源表示独占锁。State为0表示锁可用;为1表示被占用;为N表示重入的次数 |
CountDownLatch | 资源表示倒数计数器。State为0表示计数器归零,所有线程都可以访问资源;为N表示计数器未归零,所有线程都需要阻塞。 |
Semaphore | 资源表示信号量或者令牌。State≤0表示没有令牌可用,所有线程都需要阻塞;大于0表示由令牌可用,线程每获取一个令牌,State减1,线程每释放一个令牌,State加1。 |
ReentrantReadWriteLock | 资源表示共享的读锁和独占的写锁。state逻辑上被分成两个16位的unsigned short,分别记录读锁被多少线程使用和写锁被重入的次数。 |
AQS提供了对并发管理的一套模板框架,开发者并不需要了解其中的实现细节,并且其中很多方法都是final
或private
的,设计者并不希望开发者使用这些方法。AQS暴露了一些待实现的API以方便开发者解决在并发中自定义的问题,包括资源是如何被定义如何方法的。
API | 描述 |
---|---|
tryAcquire() | 排它获取(资源数) |
tryRelease() | 排它释放(资源数) |
tryAcquireShared() | 共享获取(资源数) |
tryReleaseShared() | 共享获取(资源数) |
isHeldExclusively() | 是否排它状态 |
在AQS内部使用了一个int类型的state代表了资源,并暴露出getState
,setState
,compareAndSetState
方法来对state进行修改。对于线程的阻塞和唤醒,是通过LockSupport
实现。对于等待线程的管理是AQS实现的核心,等待队列是一种严格FIFIO的队列,采用双向链表实现。基于一种CLS锁变种。CLH锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
CLH队列中的节点都是对线程的封装,节点一共有两种类型分为独占节点(EXCLUSIVE)和共享节点(SHARED)。每个节点又会有一些自己的状态。CLH的实现中会用前一个节点的属性(waitStatus
)来标识后一个节点的状态。当加入第一个线程时会默认创建一个虚拟节点,并使用虚拟节点的waitStatus
标识第一个节点的状态。
状态 | 代码 | 含义 |
---|---|---|
CANCELLED | 1 | 取消。表示后驱结点被中断或超时,需要移出队列 |
SIGNAL | -1 | 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。) |
CONDITION | -2 | Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了 |
PROPAGATE | -3 | 传播。适用于共享模式(比如连续的读操作结点可以依次进入临界区,设为PROPAGATE有助于实现这种迭代操作。) |
INITIAL | 0 | 默认。新结点会处于这种状态 |
Condition类
AQS中包含了Condition
的实现用来实现条件等待。Condition
提供了await()
方法将当前线程阻塞,并提供signal()
方法支持另外一个线程将已经阻塞的线程唤醒。Condition
需要结合Lock
使用。线程调用await()
方法前必须获取锁,调用await()
方法时,将线程构造成节点加入等待队列,同时释放锁,并挂起当前线程。其他线程调用signal()
方法前也必须获取锁,当执行signal()
方法时将等待队列的节点移入到同步队列,当线程退出临界区释放锁的时候,唤醒同步队列的首个节点。Condition
提供了类似object.wait
和notify
的线程通信机制,但是condition
支持多个等待队列,使用上更加灵活。
CountDownLatch类
CountDownLatch
采用AQS实现了共享锁的功能。其构造方法指定资源state
。线程调用await()
方法后会进入等待队列,并阻塞自身。当其他线程调用countDown()
时会将资源数减少,等资源数位0时表示没有锁。此时CountDownLatch
实现的tryReleaseShared
方法会判断资源数是否为0,如果为0,则AQS的doReleaseShared
方法被调用,此方法会唤醒等待队列的头节点,头节点被唤醒后会继续向后传播,直至唤醒所有节点。
CyclicBarrier
与之有着类似功能,但是CyclicBarrier
是可以重用的。CyclicBarrier
通过Condition
和ReentrantLock
来做实现。其关键代码在dowait
方法中,当指定数量的线程全部到达时候如果指定了后续任务,则开始运行后续任务,否则先到达的线程阻塞等待后续线程运行。当阻塞线程被中断时,抛出BrokenBarrierException
。通过reset
方法来重置CyclicBarrier
。该方法首先破坏栅栏,然后重置计数器。
Semaphore类
Semaphore
内部构造和CountDownLatch
大同小异,都是接用AQS的API,定义了公平策略和非公平策略两个内部类。其构造方法指定了许可数量和公平与否。当许可降低为0时候后续线程必须阻塞,和CountDownLatch
逻辑有些相反。这其实反映了对资源状态不同的定义。
Sync
是Semaphore
的一个内部抽象类,公平策略的FairSync
和非公平策略的NonFairSync
都继承该类。
Semaphore
对于tryAcquireShared
的实现主要是比较状态和剩余许可,当队列中有线程等待时,直接返回负数,代表失败。非公平策略则不管队列中是否有线程等待,直接尝试获取许可。否则CAS自旋将状态减少获取许可数,返回剩余状态。可以一次获取多个许可,如果剩余状态不足doAcquireSharedInterruptibly
方法会将当前线程包装成Node
节点,并加入等待队列。然后自旋尝试获取资源,如果获取失败则将前置节点设置为SINGNAL
表示自身需要被唤醒,然后阻塞线程。
线程调用release
方法归还许可,tryReleaseShared
方法通过自旋CAS来更新state,直到更新成功。成功之后doReleaseShared
方法会唤醒等待队列中的等待节点。被唤醒的节点继续原先的自旋,尝试获取许可,获得许可后将自身设置为等待队列的头节点,并传播状态尝试唤醒后续等待节点。
当资源大于1时候,Semaphore
限制了并发量,但是并不能保证数据的一致性,它并不能起到锁的作用。
ReentrantReadWriteLock类
ReentrantReadWriteLock
(以下简称RRW),也就是读写锁,是一个比较特殊的同步器,特殊之处在于其对同步状态State
的定义与ReentrantLock
、CountDownLatch
都很不同。
其构造方法仍然允许通过一个布尔值控制公平与否的策略。RRW提供了获取读锁写锁的方法,返回实例为两个实现了Lock
接口的内部类。读锁是一种共享锁,实现了AQS的共享功能。其内部调用了acquireShared
方法。
RRW仍然使用一个int类型的state表示资源的状态,但是在RRW里需要表示读状态和写状态,因此,int的高低16位分别表示两个状态。
读锁的获取过程:
- 如果写锁已被占用,并且不是当前线程独占,直接返回失败。
- 如果读锁重入数量超过65536,抛出异常。
- 如果根据公平性原则判断当前线程不需要等待并且CAS设置状态成功,返回成功。
- 如果没有返回,说明当前线程需要等待,或者CAS设置失败。此时调用
fullTryAcquireShared
一直自旋尝试CAS设置状态。
写锁是一种独占锁,线程会在判断锁被其他线程获取的时候进入等待队列并被阻塞。
读锁会通过内部类HoldCounter
来保存每个线程的读锁重入数量。释放时,会将读锁重入数量减少1。当读锁重入数为0时,调用doReleaseShared
方法来唤醒等待队列的线程。
在非公平模式下,写锁永远可以直接获取锁。这其实时性能优化的考虑,因为大多数情况写锁涉及的操作时间耗时要远大于读锁,频次远低于读锁,这样可以防止写线程一直处于饥饿状态。