JVM锁实现原理
-
- 1、CPU架构
- 2、自旋锁的诞生
-
- [2.1、SPIN ON TEST-AND-SET](#2.1、SPIN ON TEST-AND-SET)
- 2.2、TEST-AND-TEST-AND-SET
- [2.3、DELAY BETWEEN EACH REFERENCE](#2.3、DELAY BETWEEN EACH REFERENCE)
- 2.4、READ-AND-INCREMENT
- 3、MCS锁的实现
- 4、CLH锁的实现
- 5、AQS设计原理
- 6、AQS实现过程
- 7、ReentrantLock实现原理
- 8、ReentrantReadWriteLock实现原理
- 9、CountDownLatch实现原理
1、CPU架构
最初的处理器都致力于单核处理器的发展,但随着单核处理器性能已经发挥到极致,就发展了多核处理器技术。经过几十年的发展,多核处理器技术已经非常成熟,目前多核技术大多采用的是共享内存的架构。
1.1、SMP
SMP(Symmetrical Multi-Processing,对称多处理器技术),是指在一个计算机上集成了多个CPU,各个CPU之间共享统一内存子系统以及总线结构。在这种架构中,多个CPU由同一个操作系统管理与调度,并共享内存、磁盘、网卡以及其他资源,逻辑架构如图6-1所示。在程序管理调度时,系统将任务队列对称地分布在多个CPU之上,所有的处理器都可以平等地访问内存、I/O和外部中断,从而极大地提高了整个系统的任务处理能力。
由于多个CPU共享同一个内存,这就带来了高速缓存的数据一致性问题。缓存数据一致性问题需要通过硬件方式来解决,会带来很大的硬件性能损耗,而这种损耗会随着CPU的个数增加而增加。例如,有个96核CPU的服务器,每个CPU都运行一个线程,每个线程都在操作同一个变量V。其中一个CPU上的线程对V进行修改,会把消息广播到其他95个CPU上,其他95个CPU都需要处理这个消息,这会带来极大的硬件性能损耗,所以SMP技术很难组建大规模的CPU系统。

1.2、NUMA
由于SMP在扩展能力上的限制,工程师开始探究如何构建大型系统的技术,NUMA(Non-Uniform MemoryAccess,非一致性内存访问)就是探索成果之一。采用NUMA技术,可以将数十个CPU(或数百个CPU)有效地 整合到一台服务器中,实现协同工作。NUMA逻辑架构如图所示。

在NUMA的架构中,一台服务器有多个节点(Node),每个节点中有多个CPU,每个节点都有自己的内存,即本地内存。同时,每个节点上的CPU也可以访问其他节点上的内存,其他节点上的内存称为远端内存。在图中,对节点0来说,节点1、节点2、节点3上的内存都是远端内存。CPU与内存之间通过片内总线进行连接,各个节点之间访问是通过互联模块(Crossbar Switch)来进行的。在NUMA架构中,CPU访问本地内存速度非常快,访问远端内存速度仅是访问本地内存速度的20%~77%。
1.3、SMP与NUMA比较
SMP所有的资源都是共享的,它的优势是所有CPU访问内存的速度非常快,不足在于不能进行高效扩展。因为NUMA采用了多节点的结构,所以其优势在于能够高效扩展,将上百个CPU构建成一个系统,其不足在于每个CPU访问远端的内存会比较慢,时效性比较低。目前,大部分的应用服务器都是基于SMP架构设计的,而大规模的数据存储服务器则是基于NUMA架构设计的。
2、自旋锁的诞生
在多共享内存多处理器已非常成熟、CPU支持CAS硬件原语的背景下,1990年Thomas E. Anderson教授发表了名为"The Performance of Spin Lock Alternatives forShared-Memory Multiprocessors"的论文,详细阐述了自旋锁的设计方案与性能分析。这篇论文也是后面MCS、CLH、AQS等一系列锁的实现的基础理论依据。
Thomas E. Anderson教授在论文中详细阐述了4种自旋锁的设计方案。下面详细讲解每个设计方案的设计原理,以便读者对自旋锁的设计有更加深刻的理解。
2.1、SPIN ON TEST-AND-SET
SPIN ON TEST-AND-SET是最简单的自旋锁的模型。如下图所示,将锁的初始化状态设置为CLEAR状态。

使用TestAndSet来修改锁的状态,如果锁的状态被修改成BUSY,则表示获得了锁。如果获取锁失败,则循环多次尝试获取。在执行完业务逻辑后,将锁的状态修改为CLEAR来释放锁。
在伪代码中,TestAndSet需要通过CAS硬件原语来实现,CAS硬件原语是直接访问内存的,不会用到CPU的缓存机制。当多线程同时不停地通过CAS来访问内存,会造成内存总线消息拥堵,同时每个CPU都会处于满负荷的状态来运行循环操作。在锁释放时,持有锁线程的CPU也需要同其他自旋的线程的CPU来争抢同一个内存的原子性操作权限。随着CPU核数增加,并发线程数增加,自旋带来的内存争抢会更加激烈,这样会导致CPU的整体性能急剧下降。
2.2、TEST-AND-TEST-AND-SET
在SPIN ON TEST-AND-SET基础上,Thomas E.Anderson教授做了进一步的改良,变成了TEST-AND-TEST-AND-SET,伪代码如图所示。

TEST-AND-TEST-AND-SET具体是在TestAndSet操作之前增加了锁状态的查询,这样就能使用CPU的本地缓存了。如果锁是BUSY状态,则不尝试获取锁,这样大大降低了内存总线的信息堵塞。线程拿到锁了之后,会将锁状态改成LOCK,其他CPU上的本地缓存数据会失效。这个改良能够很好地解决CPU的主内存访问带来的性能问题,但CPU一直在执行空循环,仍然会浪费CPU资源。
2.3、DELAY BETWEEN EACH REFERENCE
DELAY BETWEEN EACH REFERENCE是在TEST-AND-TEST-AND-SET基础上引入了睡眠机制,如图所示。也就是每次在获取不到锁的情况下,调用Delay方法让线程进入睡眠状态。

每个CPU线程的睡眠时间是随机的,可以设定最长睡眠时间T。例如,最大时间T为1000ms,在获取不到锁的情况下,线程会随机睡眠1ms~1000ms。这样,获取不到锁的线程可以释放CPU,让CPU执行其他任务,从而提高CPU的使用效率。
这个设计虽然提高了CPU的使用效率,但又带来另一个问题:T的值到底设置多少合适?如果T的值太小,则CPU的使用效率提升并不明显。如果T的值设置太大,获取锁的时间就会有明显的延迟。其中一个CPU上的线程把锁释放了,其他CPU要过很久才能从睡眠中醒来并获取锁。
2.4、READ-AND-INCREMENT
DELAY BETWEEN EACH REFERENCE在延迟策略与避让策略上的设计与实现都比较困难,所以Thomas E.Anderson教授转变思考方向,从排队的角度来解决自旋带来的性能问题。锁由2个部分组成:一部分是锁状态数组flagsP,另一部分是锁请求计数器queueLast。在锁初始化的时候,构建一个长度为P(CPU的个数,例如有4个CPU那么P就是4)的flag数组,flags0的状态HAS_LOCK表示可以获取锁,其他数组的内容为MUST_WAIT。queueLast初始值为0。伪代码如图所示。

线程先将queueLast加1,然后通过取模运算(queueLast%P)获取到线程对应的index,然后循环观察flagsindex的状态。如果flagsindex状态为HAS_LOCK就表示线程获取到锁了。当需要释放锁时,把当前线程对应的flagsindex的状态改成MUST_WAIT,并将flagsindex+1的状态改成HAS_LOCK,这样后面线程就能获取锁了。通过队列的设计让每个CPU都只关注对应index上的flag数组的值,这样多个CPU就不会访问同一个值,不会造成内存总线的消息阻塞。
3、MCS锁的实现
在1990年Thomas E. Anderson发表了论文之后,JohnM. Mellor Crummey和Michael L. Scott在1991年发表了名为"Algorithms for Scalable Synchronization onSharedMemory Multiprocessor"的论文。论文详细阐述了另一种改进型的自旋排队锁机制:MCS锁。相比Thomas E. Anderson采用数组的形式来实现自旋排队锁,MCS锁是采用链表的形式来实现的。
整个MCS锁就是一个由单向链表构成的等待队列,队列中的每个节点称为Node,MCS锁会维护一个tail指针,该 指针会指向整个链表的尾部节点,如图所示。

Node由2个部分组成:一部分是锁的标志locked,locked为true表示等待锁,locked为false表示成功加锁;另一部分是指向后继节点的next指针。
3.1、获取锁的过程
锁的获取包含两个步骤:一是将当前线程加入等待队列,二是持续观察当前等待节点的状态。首先利用当前线程构造Node,然后通过CAS的方式将Node加入等待队列。线程需要持续观察当前节点的状态,如果当前节点的locked值为true,则持续自旋。整个获取锁的流程如图所示。

3.2、释放锁的过程
锁的释放包含两个步骤:一是通知后继节点获取锁;二是将当前节点从等待队列中移除。在锁释放的时候,已经拿到锁的线程将其后继节点的锁状态改为false,这样后继节点就获取到锁了。如果后继节点为空,释放锁的线程需要等到新节点加入才能释放锁。在通知后继节点后,会将当前节点的next指针设置为空,将当前线程从队列中移除。释放锁的流程如图所示。

3.3、MCS锁的特征
MCS锁采取FIFO原则来获取锁,也就是先发起获取锁的线程可以先拿到锁,这样可以避免饥饿式等待。每个等待锁的线程都自旋在自己节点的锁状态上,避免了集中式内存访问造成的访问争抢问题。每个锁只需要很小的内存空间,因为锁只要维护一个tail节点。无论CPU架构是否采用一致性缓存架构,该算法每次获取锁都只会产生O(1)常量级别的内存通信开销。
3.4、锁的实现案例
如下代码是一个MCS锁的示例。MCSLock内部定义了2个属性:
- 一个是Atomic-Reference的tail指针
- 另一个是ThreadLocal变量threadLocal。
AtomicReference提供了原子性的修改能力。ThreadLocal提供了线程安全地获取当前线程Node的能力。
Node有两个属性:
- 一个是Boolean变量locked,表示线程获取锁的状态;
- 另一个是Node的next指针,指向当前节点的后继节点。
locked采用volatile关键字修饰,以确保多线程的可见性。
java
public class MCSLock {
//定义tail节点
private AtomicReference<Node> tail = new AtomicReference<Node>();
//用ThreadLocal来保存线程拥有的节点信息
private volatile ThreadLocal<Node> threadLocal = ThreadLocal.withInitial(Node::new);
class Node {
//锁状态的标志
private volatile boolean locked = true;
//tail指针
private volatile Node next = null;
public boolean getLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
public void lock() {
//获取当前节点
Node currentNode = threadLocal.get();
//将当tail指针指向当前节点,并返回原来tail的值所表示的前驱节点
Node predNode = tail.getAndSet(currentNode);
//如果前驱节点为空,表示没有线程等待,直接获取锁
if (predNode == null) {
currentNode.setLocked(false);
} else {
//把前驱节点与当前节点进行连接
predNode.setNext(currentNode);
//循环等待
while (currentNode.getLocked()) {
}
}
}
public void unlock() {
Node curNode = threadLocal.get();
threadLocal.remove();
if (curNode == null || curNode.getLocked() == true) {
return;
}
//如果后继节点为空,就将tail指针指向null
if (curNode.getNext() == null && !tail.compareAndSet(curNode, null)) {
//如果后继节点为空,则循环等待
while (curNode.getNext() == null) {
}
}
//获取到后继节点
Node nextNode = curNode.getNext();
if (nextNode != null) {
nextNode.setLocked(false);
curNode.setNext(null);
}
}
}
java
public class Demo {
public static int count = 0;
public static void main(String[] args) {
MCSLock lock = new MCSLock();
new Thread(new Task(lock)).start();
new Thread(new Task(lock)).start();
try {
Thread.sleep(2 * 1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
static class Task implements Runnable {
private MCSLock lock;
public Task(MCSLock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
lock.lock();
count++;
lock.unlock();
}
}
}
}
3.5、MCS锁的不足
虽然MCS算法在获取锁的时候,每个线程都只在自己的节点上进行自旋,极大地减少了内存总线消息拥塞的情况。但它在锁释放的时候也有明显的性能问题。并且锁释放的逻辑非常复杂,如图所示。

场景1:此刻有多个线程正在同时申请锁并且已更新tail指针,而释放线程节点的next指针还没有指向后继节点,而处于null状态,且tail也不是指向释放线程的节点。这个时候,释放线程的节点一直等待其他线程节点与当前线程的节点连接起来,才将后继节点的locked域设置为false。
场景2:当前线程后面没有节点。只需要调用AtomicReference类的compareAndSet()方法,将tail指针指向null,并销毁当前节点即可。
场景3:此刻有其他线程已经申请完锁,并进入线程自旋等待的状态,即要释放锁的线程节点的next节点不为非空,就可以直接将后继节点的locked域设置为false,以便后继节点退出自旋,从而获取到释放的锁。
4、CLH锁的实现
1933年,Travis S. Craig教授发表了一篇名为"BuildingFIFO and Priority-Queuing Spin Locks from AtomicSwap"的论文,阐述了一种可以支持FIFO与权重比的自旋锁设计方案,该方案在SMP与NUMA架构下的性能都比较出色。在Travis S. Craig论文的基础上,1994年Anders Landin与Erik Hagersten发表了名为"Efficient Software Synchronization on Large Cache CoherentMultiprocessors"的论文,介绍了LH锁的实现。但这两篇论文在底层原理与思路上基本是一致的,只是表述的形式各有不同。工程人员结合Travis S. Craig、AndersLandin与Erik Hagersten的论文共同实现了一个锁,称为CLH锁(CLH来自Craig、Landin、Hagersten的首字母)。
CLH锁是一个由单向链表构成的等待队列,队列中存放的是要获取锁的Request请求。Request请求由3部分组成:当前节点指针curNode、前驱节点指针preNode,以及锁请求节点。锁请求节点中定义了锁状态标志locked:locked为true表示待获取锁;locked为false表示已获取锁。每个线程都会通过自旋方式来观察前驱节点上的locked的值。
同时CLH锁还会维护一个tail指针指向最后一个节点。整 个CLH锁的结构如图所示。

4.1、获取锁的过程
当线程获取锁时,CLH会先构造锁请求的节点curNode,并将curNode的locked状态设置成true,表示需要获取锁。然后通过tail指针获取队列尾部的节点,并将队列的尾部节点作为当前节点的前驱节点,通过CAS方式将tail指针指向curNode。获取锁的线程会自旋观察preNode节点的locked标志。如果preNode的locked为false就表示当前线程获取到锁了。获取CLH锁的流程如图所示。

4.2、释放锁的过程
获取当前线程的锁节点,然后将当前节点的locked设置为false,这样后继节点就能自动获取到锁。
4.3、CLH锁与MCS锁的对比
虽然Craig、Landin与Hagersten的两篇论文里都引用了MCS的论文,但并没有明确阐述CLH锁与MCS锁的明确区别。但从具体实现上,CLH锁与MCS锁还是有几个明显差别的,如表所示。

4.4、锁的实现案例
如下代码是一个CLH锁的Java语言实现示例。CLHLock定义了3个属性:一个是tail指针,指向链表的尾节点;另一个是curNode,表示当前节点,还有一个是preNode,表示前驱节点。Node定义了boolean型的locked变量,用来表示锁的状态,true表示需要等待锁,false表示不持有锁。locked属性采用volatile关键字修饰,以确保多线程的可见性。
java
public class MCSLock {
//定义tail节点
private AtomicReference<Node> tail = new AtomicReference<Node>();
//用ThreadLocal来保存线程拥有的节点信息
private volatile ThreadLocal<Node> threadLocal = ThreadLocal.withInitial(Node::new);
class Node {
//锁状态的标志
private volatile boolean locked = true;
//tail指针
private volatile Node next = null;
public boolean getLocked() {
return locked;
}
public void setLocked(boolean locked) {
this.locked = locked;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
public void lock() {
//获取当前节点
Node currentNode = threadLocal.get();
//将当tail指针指向当前节点,并返回原来tail的值所表示的前驱节点
Node predNode = tail.getAndSet(currentNode);
//如果前驱节点为空,表示没有线程等待,直接获取锁
if (predNode == null) {
currentNode.setLocked(false);
} else {
//把前驱节点与当前节点进行连接
predNode.setNext(currentNode);
//循环等待
while (currentNode.getLocked()) {
}
}
}
public void unlock() {
Node curNode = threadLocal.get();
threadLocal.remove();
if (curNode == null || curNode.getLocked() == true) {
return;
}
//如果后继节点为空,就将tail指针指向null
if (curNode.getNext() == null && !tail.compareAndSet(curNode, null)) {
//如果后继节点为空,则循环等待
while (curNode.getNext() == null) {
}
}
//获取到后继节点
Node nextNode = curNode.getNext();
if (nextNode != null) {
nextNode.setLocked(false);
curNode.setNext(null);
}
}
}
java
public class CLHDemo {
public static int count = 0;
public static void main(String[] args) {
CLHLock lock = new CLHLock();
new Thread(new Task(lock)).start();
new Thread(new Task(lock)).start();
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
static class Task implements Runnable {
private CLHLock lock;
public Task(CLHLock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
lock.lock();
count++;
lock.unLock();
}
}
}
}
//20000
5、AQS设计原理
我们已经了解了MCS锁与CLH锁的设计原理,通过对比可以清晰地发现:CLH锁的优点比较明显:入队和出队速度快、无锁且无阻塞、检测是否有线程在等待也很快,并且每个等待线程只关注前一个线程的状态,内存竞争比较少。但线程在获取CLH锁时,CPU是处于自旋状态,并没有释放。
2004年,Java大神Doug Lea发表了一篇名为"Thejava.util.concurrent Synchronizer Framework"的论文,详细阐述了Java的AbstractQueuedSynchronizer(抽象队列同步器,简称AQS)的设计原理与实现方案。
JSR166规范提出了很多线程并发控制的需求,为了满足JSR166在并发控制上的要求,Doug Lea在CLH的基础上设计了AQS。AQS能够确保多线程在同时获取锁的情况下,总体性能消耗是恒定的,不会发生随着获取线程的增加,导致锁的性能明显下降的情况。同时确保了CPU使用效率、内存流量和线程调度开销总体可控。
5.1、功能与特征
AQS一般包含两类方法:
- 一类是获取锁的方法
- 另一类是释放锁的方法
在获取锁时,AQS会阻塞调用的线程,直到同步状态允许其继续执行。在锁释放时,AQS则通过CAS方式改变同步状态,并唤醒一或多个被阻塞的线程来获取锁。在上述两类操作的基础上,AQS还支持如表所示的特征。

5.2、设计原理
AQS由3个组件相互协作完成:同步状态、线程的阻塞与解除阻塞、线程等待队列。每个组件都有独立的实现,然后通过组合模式进行耦合。这样保证了组件的独立性,同时保障了组合的灵活性。
1. 同步状态
同步状态state是int类型的变量,这与前面所有锁的实现算法都不同。MCS锁与CLH锁都是用boolean类型的变量来表示锁状态的。因为AQS需要支持独占与共享两种锁模式,所以state采用int类型来表示。state初始值表示有多少个线程能同时获取锁:state为1,表示独占锁模式,表示只有1个线程可以获取到锁;state为N(N > 1)的时候为共享锁模式,表示N个线程可以同时获取到锁。state是用volatile关键字修饰的,这样确保了多线程的可见性。state的修改是通过CAS方式实现的,这样可以确保操作的原子性。
2. 线程的阻塞与解除阻塞
在AQS里,线程的阻塞和唤醒是通过LockSupport类来实现的。在锁等待的时候,AQS会调用LockSupport的park方法将等待线程阻塞。在锁释放的时候,AQS会调用LockSupport类的unpark方法唤醒等待中的线程来获取锁。
3. 队列的管理
在等待队列的设计上,相比CLH而言,AQS做了非常大的改变,首先把单向链表改成了双向链表,每个线程的等待节点有两个指针:指向前驱节点的prev指针和指向后继节点的next指针。同时AQS增加了同步队列的头节点指针head和尾节点指针tail,这样就能够通过头节点指针或尾节点指针,快速找到队列中的任何一个节点。队列的指针都是采用volatile关键字修饰的,这样确保了多线程的可见性,同时对这些指针的修改都会采用CAS的方式,确保多线程修改的原子性。同步队列的结构如图所示。

每个节点上增加了status来表示节点的状态,注意这里不是表示锁的状态,而仅仅表示节点的状态。waitStatus状态值如表所示。

设计status的目的是减少LockSupport类的unpark方法的调用,因为线程已经取消了,就不需要调用unpark方法来唤醒线程了。线程唤醒最终是需要Linux系统内核来完成的,无效的调用会增加系统的运行成本,会带来上下文切换开销。
5.3、设计模式
AQS采用了模板方法的设计模式,对通用功能做了具体实现。AQS实现了3个方面的通用功能,如表所示。

同时AQS也提供了一些子类可以扩展的功能,方便子类按照业务场景进行自定义实现,如表所示。

6、AQS实现过程
6.1、逻辑架构
AQS自上而下可以大致分为5层:API层、扩展接口层、逻辑实现层、线程与队列管理层、状态与队列管理层。
API层是锁获取与释放的入口,方便调用者快捷地使用AQS获取锁。扩展接口层提供了tryAcquire、tryAcquireShared、tryRelease、tryReleaseShared等扩展接口。子类可以通过实现这些方法来定制锁的获取与释放逻辑。逻辑实现层是锁获取的核心逻辑实现,用来控制锁获取、线程等待、队列等待、锁释放等核心逻辑。线程与队列管理层主要负责线程与队列节点之间的映射与管理。最底层分为两部分:一个部分是锁状态的管理功能;另一部分是等待队列的管理功能,包含队列的头部节点管理、尾部队列管理、节点入队管理等功能。
6.2、状态管理
AQS需要支持共享锁的模式,所以同步状态采用int类型的变量state来表示。state为1表示独占锁模式,只有1个线程可以获取到锁。state为N(N>1)的时候表示共享锁模式,即N个线程可以同时获取到锁。AQS进行状态管理的实现如代码所示。


state是用volatile关键字来修饰的。getState与setState方法是标准的字段读写功能。compareAndSetState方法是通过Unsafe类的compareAndSwapInt方法来实现线程修改的原子性的。通过volatile+CAS的方式确保多线程修改的安全性。
6.3、队列管理
AQS采用了双向链表来实现线程等待队列,内部定义了两个指针:head指针与tail指针。head指针指向队列的头节点,tail指针指向队列的尾节点。这样设计的好处是能够从head节点向后遍历,也能通过tail节点向前遍历,方便进行链表的管理。AQS等待队列如图所示。

1. Node定义
Node是等待队列中的等待节点。Node有2个指针:prev指针指向前驱节点,next指针指向后继节点。prev指针与next指针都是用volatile关键字修饰的。指针的修改是采用CAS机制实现的。通过volatile+CAS组合实现多线程的安全性。Node有2种模式:SharedNode(共享锁)与ExclusiveNode(独占锁)。
Node还定义了2个变量:waiter用来表示等待线程,status用来表示Node的等待状态。status用来减少对LockSupport类的unpark方法的调用。如果当前节点的status是CANCELLED,表示当前线程已经取消了获取锁的请求,就不需要调用unpark方法来唤醒线程了。如下代码是Node定义的代码。

2. head与tail指针
AQS定义了两个指针:head指针指向头节点,tail指针指向尾节点。head指针与tail指针都是采用volatile关键字修饰的。tryInitializeHead方法通过CAS方式来初始化头节点,以确保多线程修改的原子性。AQS队列头/尾节点管理如代码所示。

casTail方法用来设置队列的尾节点tail,会存在多线程同时进入等待的情况,所以必须通过CAS的原理来确保修改的原子性。
3. 添加节点
enqueue方法就是用来向队列中添加节点的,AQS队列添加节点的实现如代码所示。


在往队列添加新节点的时候,可能会有两种情况:一种是队列为空的时候,如图所示;

另一种是队列中已经有节点了,如图所示。

在队列为空的时候,首先调用tryInitializeHead方法构造一个空的节点,然后将head指向空节点,tail指针指向head节点。
如果队列不为空,就调用casTail方法把当前节点增加到队列尾部,然后把原来尾节点的next指针指向当前节点,最后返回尾节点。当多线程可能会同时向队列尾部去添加节点,通过循环的方式来多次尝试处理。
6.4、线程与队列管理层
线程与队列管理层主要负责将线程对象Thread转化成等待节点的Node,并管理队列的逻辑。
1. 唤醒后继节点的线程
signalNext方法的功能是唤醒后继节点的线程,AQS唤醒后继节点线程的实现如代码所示。


signalNext方法首先会判断后继节点是否为空。如果后继节点不为空,它会将后继节点的状态设置成WAITING,并唤醒后继节点的线程。
2. 唤醒共享锁的等待节点线程
signalNextIfShared方法的功能是唤醒共享锁的等待节点线程,如代码所示。

3. 移除队列中的无效线程
cleanQueue方法的功能是移除等待队列中的无效节点,其实现如代码清单6-9所示。当新线程加入等待队列后,AQS会调用cleanQueue方法来清理队列中的无效节点。cleanQueue方法会从队列尾部开始向前遍历,去掉中间取消的无效节点。当遍历到队列的头节点时,cleanQueue会唤醒头节点的线程来获取锁。


在下图中,thread-4是当前节点,thread-3是当前线程的前驱节点。thread-2与thread-3都是CANCELLED状态。

cleanQueue方法会依次删除掉等待队列中的thread-2、thread-3两个节点。然后把当前节点prev指针指向thread-1节点,把thread-1节点的next指针指向当前thread-4节点,最终结果就变成了如下图所示的状态。

4. 取消锁请求
cancelAcquire方法会将当前节点的状态设置为CANCELLED,即取消锁请求,如代码所示。然后调用cleanQueue方法清理队列中的无效节点。

6.5、锁的逻辑实现
线程与队列管理层上面是锁的逻辑实现层,这一层主要实现独占锁与共享锁的核心逻辑。
acquire方法主要的功能是获取锁:如果获取成功就直接返回,如果获取失败就进入队列进行等待。整个在逻辑实现上有点复杂,需要读者重点关注、反复思考,锁获取流程如图所示。

锁获取流程中有2个细节需要注意:一是整体获取锁的逻辑是通过for循环来实现的,也就意味着需要通过多次尝试来完成;二是因为AQS采用了FIFO规则,所以只有头节点才有机会获取锁。AQS获取锁流程实现如代码所示。


6.6、扩展接口层
扩展接口层提供了子类扩展实现的接口,在AQS里没有给出具体实现,该层只是用来控制锁状态的接口。tryAcquire方法用来获取独占锁的状态,tryRelease方法用来修改独占锁的状态(释放),tryAcquireShared方法用来获取共享锁的状态,tryReleaseShared方法用来修改独占锁的状态(释放)。
6.7、API层
API层作为整个锁的入口层,提供了获取锁与释放锁的所有功能接口。为了方便读者更好地理解AQS的API,我按照获取锁的方式、是否支持中断、等待方式3个维度列出了6种获取锁方法的差别,详情如下所示。

acquire方法是获取独占锁的入口方法。acquire方法先调用tryAcquire方法来获取锁状态:如果成功就返回;如果失败,就将当前线程加入等待队列。AQS独占锁获取接口实现如代码所示。

tryAcquireNanos方法是获取独占锁的入口方法。tryAcquireNanos方法在acquireInterruptibly方法的基础上增加了等待超时自动结束等待的功能。AQS独占锁获取、支持超时接口如代码所示。


acquireShared方法的功能是获取共享锁。acquireShared方法通过调用tryAcquireShared来获取共享锁的状态,如果获取成功则直接返回。如果不成功,则调用doAcquireShared方法来排队获取共享锁。AQS共享锁获取接口如代码所示。

acquireSharedInterruptibly方法是在acquireShared方法基础上增加了中断的功能,会判断当前线程是否中断,如果中断则抛出异常。AQS共享锁获取、支持中断接口的实现如代码所示。

tryAcquireSharedNanos方法在acquireSharedInterruptibly方法的基础上增加了超时功能。releaseShared方法是释放共享锁的API,会先调用tryReleaseShared方法将锁修改为释放状态,然后调用doReleaseShared方法来通知等待队列中的后续节点来获取锁。
7、ReentrantLock实现原理
ReentrantLock是可重入的互斥锁。ReentrantLock支持公平锁和非公平锁两种获取锁的模式。
可重入性是指一个线程可以重复获取同一个锁。synchronized具有可重入性,用synchronized修饰的递归方法,当线程在执行时可以反复获取到锁,而不会出现死锁的情况。Reentrant-Lock也是如此,在调用lock方法时,如果当前线程已经获取到该锁,还能再次调用lock方法获取锁,而不被阻塞。
公平锁就是指锁的获取策略相对公平,当多个线程在获取同一个锁时,必须按照锁的申请时间来依次排队获取,不能插队。非公平锁则不同,获取锁的线程不管前面有没有线程排队,都会直接获取锁,如果获取不到锁再去排队。公平锁能够保证获取锁的公平性,但非公平锁能够提高整体效率。ReentrantLock默认采用非公平锁,但可以通过带boolean参数的构造方法指定使用公平锁。
ReentrantLock实现了Lock接口,提供了互斥锁获取与释放的方法,如表所示。

同时ReentrantLock自身也扩展了一些方法让我们能够更好地使用互斥锁,具体方法如表所示。

7.1、源码简介
ReentrantLock实现了Lock接口获取锁与释放锁的相关方法,定义了同步器Sync。Sync继承了AbstractQueuedSynchronizer,是AQS的具体实现。Sync有两个子类:NonfairSync(非公平锁同步器)与FairSync(公平锁同步器)。NonfairSync与FairSync重写了lock方法与tryAcquire方法。UML图如图所示。

7.2、基础同步器
Sync通过继承AbstractQueuedSynchronizer获得了锁获取、锁释放、锁排队等待的能力,同时对AbstractQueuedSynchronizer的扩展方法tryRelease进行了具体实现。
tryRelease方法的功能是修改锁为释放状态。tryRelease方法实现如代码所示。

tryRelease方法的执行流程如下。
- 将AQS的同步器状态state的值减1。
- 判断当前线程是否已经获取到锁,如果没有获取到锁,则直接抛出异常,因为只有获得锁才能释放锁。
- 如果state减1后的值为0,说明需要真正释放锁,则调用setExclusiveOwnerThread方法将锁的持有线程设置为空,调用setState方法将锁状态state值设置为0。
- 如果减1后的值不为0,说明当前线程通过重入锁的方式多次获取了锁,只修改锁的状态,并不会真正释放锁。
公平锁策略与非公平锁策略都会调用tryLock方法尝试获取独占锁。tryLock方法如代码所示。

lock方法如代码所示。

7.3、非公平锁策略
NonfairSyn是非公平锁策略的实现类,实现了Sync类的lock方法与tryAcquire方法。tryAcquire方法实现如代码所示。

tryAcquire方法首先调用compareAndSetState方法直接将state的值修改为1。如果修改成功,则表示获取到了锁,然后调用setExclusiveOwnerThread方法将当前线程设置为锁的拥有者。如果修改失败,则调用AbstractQueuedSynchronizer的acquire方法通过排队等待来获取锁。
7.4、公平锁策略
FairSync是公平锁策略的核心实现类,实现了父类的tryAcquire方法。tryAcquire方法如代码所示。

tryAcquire方法只有在锁空闲,且没有线程等待的情况下才会获取锁,遵从了AQS的FIFO原则。
7.5、ReentrantLock实现
ReentrantLock的默认构造函数采用的是非公平锁策略NonfairSync。ReentrantLock获取与释放锁的功能都是通过同步器(NonfairSync、FairSync)来实现的。ReentrantLock获取锁的方法如代码清单所示。

8、ReentrantReadWriteLock实现原理
ReentrantLock适合只有一个线程运行的场景。而实际的业务会有资源读写的场景,在没有写操作的情况下,多个线程可以同时读取一个共享资源,但是有线程在执行写入操作的情况下,其他线程就不允许进行读写操作了。针对这种场景,Java提供了Reentrant-ReadWriteLock,它表示两个锁:一个是读操作相关的锁,称为读锁,读锁是共享锁;另一个是写操作相关的锁,称为写锁,写锁是互斥锁。ReentrantReadWriteLock采用读写分离的策略,允许多个线程同时获取读锁。
ReentrantReadWriteLock支持公平锁与非公平锁、锁重入、写锁降级等功能。公平锁是指如果前面有线程获取锁就排队等待,非公平锁是指线程优先尝试让自己获取锁。锁重入是指线程在获取了读锁后还可以获取读锁,线程在获取了写锁之后既可以获取写锁又可以获取读锁。锁降级是指写锁降级为读锁,一个线程先获取写锁进行写操作,然后获取读锁进行读取操作,最后释放写锁。但是从读锁升级为写锁是不允许的。
8.1、设计模式
ReentrantReadWriteLock内部组合了ReadLock与WriteLock。如图6-30所示,Reentrant-ReadWriteLock实现了ReadWriteLock接口。ReadLock与WriteLock实现了Lock接口,并对其中锁获取与释放的功能给出了具体的实现。基础同步器Sync继承了AQS,并对AQS的锁状态获取与释放的扩展接口做了具体实现。
NonfairSync是非公平锁的实现,FairSync是公平锁的实现,NonfairSync与FairSync都继承了Sync类。同时ReadLock与WriteLock引用了同一个基础同步器Sync。ReentrantReadWriteLock的UML图如图所示。

8.2、锁状态设计
AQS锁的状态state是用int表示的,Java的int是32位的。ReadLock与WriteLock共用了基础同步器Sync,也共用了一个锁状态state。ReentrantReadWriteLock采用了一个巧妙的设计,用int的低16位表示写锁的状态,高16位表示读锁的状态。整个设计如图所示。

读锁是使用高16位表示的,所以读锁计数基本单位是1的高16位,即1左移16位(1<<16),变成65536,每次成功获取读锁都加65536。例如,我们获取到了3次读锁,就相当于65536×3=196608,转换成左移公式就是3<<16位是196608。读锁状态变更如图所示。

下方代码是读锁状态变更相关的代码。


SHARED_SHIFT表示偏移的位数是16位,SHARED_UNIT就是1左移16位。线程每次成功获取读锁都会将state的值与SHARED_UNIT相加,同时每次释放读锁都会将state的值减去SHARED_UNIT。sharedCount方法通过将state右移16位来获取读锁的次数。
写锁采取的是低16位,写锁数值范围是0~65535,每次获取锁的时候都将state加1,每次释放锁时将state减1就可以了。写锁的状态变更如图所示。

同时在将锁状态state换算成写锁次数时采用"&"运算,即计算state&65535。65535的二进制就是111111111111111,写锁的计算逻辑如代码所示。

EXCLUSIVE_MASK表示低16全部为1的情况,即值为65535。exclusiveCount方法的功能是计算state&EXCLUSIVE_MASK,以获得写锁的次数。
8.3、共享锁重入次数设计
state虽然能表示读锁的次数,但是没办法统计每个线程获取了多少次锁。为了更好记录每个线程拿了几次读锁,设计了HoldCounter(锁计数器)。HoldCounter内部定义了2个变量:一个是count,用来表示线程获取锁的次数;另一个是tid,用来表示获取锁的线程ID。线程获取锁计数器HoldCounter的实现如代码所示。

线程获取锁计数器本地缓存如代码所示。ThreadLocalHoldCounter用于缓存线程的HoldCounter对象。

8.4、获取写锁状态
tryAcquire方法的功能是获取写锁状态,只有其他线程没有获取锁,当前线程才能获取写锁。获取写锁状态的实现如代码所示。

tryAcquire核心逻辑如下。
- 调用getState方法获取state的值c,然后调用exclusiveCount方法转换出写锁的状态值w。
- 如果state值不为0,表示已经有线程获取锁了(可能是读锁,也可能是写锁)。接着判断写锁状态w是否为0:如果w为0,表示写锁是空闲的。接着判断当前线程是否获取到读锁了:如果不是,则直接返回false;如果是当前线程获取的读锁,可以进行锁升级,同时获取到写锁。
- 如果state为0,说明锁是空闲的,就先调用writerShouldBlock判断下是否需要阻塞排队,如果不需要,则直接调用compareAndSetState方法更新锁状态。
8.5、释放写锁状态
tryRelease方法的功能是释放写锁状态。tryRelease方法首先判断当前线程是否持有写锁,如果不持有,则抛出异常。接着将写锁状态减1,然后调用exclusiveCount获取释放后的锁状态,如果锁状态为0表示需要释放锁,最后修改state值。释放写锁状态的实现如代码所示。

8.6、获取读锁状态
tryAcquireShared方法用于获取读锁状态,只有在没有线程获取写锁或者当前线程获取了写锁才能获取读锁的状态。获取读锁状态实现如代码所示。


tryAcquireShared方法核心逻辑如下。
- 获取当前锁状态值state,通过状态值判断是否有线程获取到写锁。如果是其他线程获得了锁,则直接返回。
- 如果锁是空闲的或者当前线程已经获取到读锁,则可以获取写锁。
- 如果读锁的次数没有超出MAX_COUNT限制,则尝试直接将state值加上65536。如果修改成功,表示成功拿到锁了。
- 如果当前线程成功获取到锁,则更新firstReader与firstReaderHoldCount两个计数器的值。
8.7、释放读锁状态
tryReleaseShared方法用于将读锁状态修改为释放,有2个功能:一是更新读锁线程计数器,二是更新全局锁状态state。具体实现如代码所示。


tryReleaseShared方法的处理流程如下。
- 判断当前线程是不是第一个获取锁的线程,如果是,则将firstReaderHoldCount的值减1。
- 判断当前线程是不是最近一次获取到锁的线程,如果是,则将cachedHoldCounter的值减1。
- 从ThreadLocal中获取当前线程的读计数器,然后将计数器的值减1。如果当前线程获取锁的次数为0,就将计数器从ThreadLocal中移除,加快内存回收。
- 将state的值减去SHARED_UNIT(65536),释放当前线程持有的读锁状态。
8.8、获取写锁
tryWriteLock方法的功能是尝试快速获取写锁,如果获取到锁了就返回成功标识,如果获取锁失败了就进入队列排队等待。获取写锁的实现如代码所示。

8.9、获取读锁
tryReadLock方法的功能是快速获取读锁。该方法的核心逻辑是根据state判断当前线程是否具备获取读锁的条件,如果具备条件就直接获取。代码是tryReadLock方法的具体实现。

8.10、ReentrantReadWriteLock实现
ReentrantReadWriteLock采用了组合模式,Sync提供了读锁与写锁的所有功能,ReadLock与WriteLock都是调用Sync的方法来实现锁功能。ReentrantReadWriteLock的构造方法会先定义好Sync锁同步器,然后用同一个锁同步器来构造ReadLock与WriteLock对象。
9、CountDownLatch实现原理
CountDownLatch是线程同步工具,它能协调一组线程来共同完成一个任务。Count Down表示倒数的意思,Latch表示门闩的意思,很形象地表达了这个锁的含义。CountDownLatch的构造函数需要传入一个整数n,n表示有n个线程任务需要执行。每个线程执行完任务之后都要将n减1,在n倒数到0之前,主线程需要等待,当n为0时主线程才继续往下执行。
如下代码是CountDownLatch的使用示例。
CountDownLatchTest实现了Runnable接口,并向构造函数传入了一个CountDownLatch锁对象,该对象在run方法里执行完逻辑之后,调用了CountDownLatch的countDown方法。
CountDownLatch主要有两类方法:一类是countDown方法,另一类是await方法。countDown方法用于将锁状态减1,一般在任务线程执行完任务之后调用。await方法会让当前线程处于等待状态,一般是主线程调用。
这里有两个地方需要注意:一是countDown方法并没有限制一个线程调用的次数,同一个线程可以多次调用countDown方法,每次都会将锁状态减1。二是await方法没有限制调用的线程数,如果多个线程调用await方法,那么这几个线程都将处于等待状态,并且等待同一个锁。CountDownLatch方法如表所示。

9.1、设计原理
CountDownLatch内部集成了基础同步器Sync,Sync继承了AQS,并对AQS的共享锁状态获取与释放的扩展方法进行了具体实现。CountDownLatch的UML图如图所示。

但是CountDownLatch的锁获取与锁释放逻辑与正常的共享锁获取与释放逻辑有比较大的差异。如下图所示,正常共享锁获取锁会将锁状态值减1,而释放共享锁会将锁状态值加1。

获取锁状态的时候,CountDownLatch会判断锁状态state是否为0,为0表示获取锁成功,不为0则进入等待队列等待。释放锁的时候,CountDownLatch会将state值减1。这样初始的锁状态state为n,则n个线程释放锁之后,state的值为0,这样等待的线程就能获取到共享锁了。
9.2、Sync源码分析
Sync实现tryAcquireShared方法与tryReleaseShared方法。tryAcquireShared方法的功能是获取共享锁状态,获取共享锁状态的实现如代码清单所示。

tryReleaseShared方法首先调用getState方法获取当前锁状态state的值,如果state为0就不用处理了。如果state不为0就将state值减1,最后调用compareAndSetState通过CAS线程安全地修改state的值。释放共享锁状态的实现如代码所示。

9.3、CountDownLatch源码分析
如下代码所示,CountDownLatch的构造函数需要传递一个表示锁数量的count变量,构造函数利用count变量构造Sync对象实例。

CountDownLatch提供了2个等待方法:一个是不带等待时间的,另一个是带等待时间的。不带等待时间的await方法是调用AQS的acquireSharedInterruptibly方法实现的,带等待时间的await方法是调用AQS的tryAcquireSharedNanos方法实现的。等待方法的实现如代码所示。

countDown方法就是调用Sync的releaseShared方法实现对锁状态值减1操作的,Count-DownLatch计数器减1的实现如代码所示。
