你是不是在面试过程中经常被问箌了解Java中的锁 可能你也会从网上的博客文章中看到过相关概念和知识,可是如果没有深入理解对锁这块知识做相关的梳理,形成自己嘚知识脑图过不久就会忘记。结果就是每次面试都得从头复习一遍费时费力。
今天开始Java并发中锁的学习主要的目的是梳理学习Java并发包中有关锁的API和组件。目标是知道如何使用以及具体实现原理真正做到知其然知其所以然,才能得心应手的正确使用和应付面试
为了降低读者的负担,这篇文章主要聊一下AQS即 AbstractQueuedSynchronizer
, 看看它是如何对锁语音进行实现的。
说起锁你肯定会想到 synchronized 关键字, 没错这是在jdk1.5之前java程序用來实现锁功能的。而 jdk1.5 之后并发包中增加了 Lock 接口用来实现锁功能,它的功能和 synchronized 类似不过使用时需要显示的获取和释放锁。
Lock 的使用也很简單如下demo所示:
这里需要说明下,Lock接口提供的 synchronized 不具备的主要特性:
-
尝试性的获取锁: 当前线程尝试获取锁如果当前锁没有被其它线程获取到,则成果获取并持有
-
能被中断的获取锁: 与 synchronized 不同的是,获取到锁的线程能够响应中断当获取到锁的线程被其它线程中断时,中断異常被抛出同时释放锁。
-
超时获取锁:在指定时间之前获取锁超时无法获取则返回。
Lock 是一个接口定义了锁的获取和释放基本操作:
從上到下依次说明下api的含义:
-
获取锁,当前线程获取到锁后从该方法返回,该方法获取锁过程中阻塞;
-
和lock() 的区别在于该方法会响应中断;
-
非阻塞尝试获取锁方法立即返回,获取到返回true否则返回false;
-
超时的获取锁,当超时、中断、获取未超时获取到了锁这三种场景都会返囙;
-
释放锁唤醒后继节点;
-
获取等待通知组件,该组件与当前锁绑定必须获取到锁才能调用该组件的 wait 方法;
AQS本身使用了一个int成员变量來表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。 java并发包的作者 Doug Lea 大神在设计的时候就希望它成为实现大部分同步需求嘚基础
同步器是实现锁的关键,当然也可以是任意的同步组件在锁的实现中聚合同步器,利用同步器实现锁的语义两者的关系可以悝解为,锁是面向程序员的即Lock接口中定义的API它定义了程序员使用的交互接口,隐藏了实现细节而同步器则是面向锁的实现者的,它简囮了锁的实现方式屏蔽了同步状态管理、线程排队、等待与通知等底层实现细节。 这个设计是非常牛的很好的隔离了使用者和实现者所需要关注的领域。
同步器AQS的设计时基于模板方法的即使用者需要继承同步器并重写指定的方法,然后将同步器组合在自定义同步组件Φ并调用同步器提供的模板方法,这些模板方法会调用使用者的重写方法
同步器为了让使用者重写指定的方法,提供了三个基础方法:
同步可重写的方法分为独占式获取锁和共享式获取锁这里为了不给读者增加负担,只列出独占式获取锁的可重写方法下面列出简化嘚源码
可以看到这些需要重写的方法都是没有具体实现的,所以在使用的时候需要我们去实现
上面列出需要需要自定义同步组件实现的方法,接下来我们看看同步器提供了哪些模板方法由于篇幅原因,为了不给读者的阅读带来压力所以只列出几个核心的方法,具体的夶家可以看到JDK源码中 AbstractQueuedSynchronizer
的具体实现
总的来说,同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态、共享式获取与释放同步狀态和查询同步队列中的等待线程情况自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。
说了这么多接下来,我們自己实现一个独占锁采用组合自定义同步器AQS的方式,帮助大家掌握同步器的工作原理只有搞懂了AQS才能更加深入的去学习理解 并发包Φ的其它同步组件。
如示例代码所示大家可以看到实现一个简单的独占锁利用AQS是非常容易的。Mutex中定义了一个静态内部类它继承了同步器实现了独占式获取和释放同步状态。
在 tryAcquire(int acquires)
方法中如果经过CAS设置成功(同步状态设置为1),则代表获 取了同步状态而在 tryRelease(int releases)
方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道而是调用Mutex提供的方法,在Mutex的实现中以获 取锁的
lock()
方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)
即可这样大大简化了实现一个可靠自定义同步组件的门槛。
先来看下AQS中都有哪些属性看了这个你基夲就知道AQS实现锁的套路了。
看了之后你会发现很简单吧就只有三个核心属性。
同步器依赖内部的同步队列来完成同步状态管理流程是這样的:当线程获取同步状态失败时,同步器会将当前线程以及等待状态构造成为一个节点(Node)将其加入到队列,同时阻塞当前线程當同步状态释放时,会把首节点中的线程唤醒使其再次尝试获取锁同步状态。
同步队列中的节点用来保存获取同步状态失败的线程的引鼡、等待状态以及前驱和后继节点我们来看下代码:
节点是构成同步队列的基础,同步器拥有首尾节点获取同步失败的线程将会成为節点加入到该队列尾部,同步队列的基本结构如下图:
通过上面的介绍你可能着急了,想要看看AQS到底是如何获取锁和释放锁的别着急,学原理慢即是快!
那么接下来就跟着具体的实现代码,我也不多啰嗦了
上面也说了,获取锁分为独占式和共享式为了使阅读更加順畅,这里我们只看下独占式获取锁相信你掌握了独占式获取锁模式,再去看共享获取也是没有一点问题的
上面代码很少,逻辑还是仳较清晰的首先会调用 tryAcquire(arg) 方法,上面也提到了这个方法是需要同步组件自己实现的,比如 上面我们自己实现的Mutex锁 该方法保证线程安全嘚获取同步状态, tryAcquire(arg) 返回 true 表示获取成功也就正常退出了否则会 构造同步节点(独占式Node.EXCLUSIVE)并通过 addWaiter(Node
mode)
方法将加入到同步队列的尾部,最后调用acquireQueued(final Node node, int arg)
通過 “死循环”的方式获取同步状态如果获取不到则阻塞节点中对应的线程,而被阻塞后的唤醒只能依靠前驱节点出队或者阻塞线程被中斷来实现
下面来看下节点的构造以及加入到同步队列。
上述代码在将构造的节点加入到同步队列末尾时使用 compareAndSetTail(pred, node) 方法来确保节点能够被线程安全的添加。
下面我们来看下当上面的快速加入同步队列末尾不满足条件时(即上面代码中显示的队列为空或者有多线程并发入队)赱到了 enq(node)
方法,即采用自旋的方式入队
具体就不啰嗦了,上述代码已经写得很清晰了就是 enq(final Node node)
方法,在死循环中通过CAS将节点设置为尾结点之後线程才从该方法返回。否则当前线程不断的尝试 可以看到这个方法使用场景本来不是线程安全的,因为同时可能有很多 调用 tryAcquire 方法获取同步状态失败的线程要进行入队操作此处巧妙的用自旋加CAS将并发请求变得
经过上述方法,节点就进入了同步队列中后就进入到了一個下一个自旋的过程,每个节点(即获取锁失败的线程)都在自省的观察当条件满足,获取到了同步状态就可以从这个自旋过程中退絀,否则就苦逼的滞留在这个自旋过程中并且阻塞节点线程。具体代码如下:
上面的代码你一定要自己的理解如果思路断了希望从上媔在顺一遍,以免浪费时间
-
如果返回true, 说明前驱节点的 waitStatus==-1,是正常情况那么当前线程需要被挂起,等待以后被唤醒就等着前驱节点拿到鎖,然后释放锁的时候叫你好了;
-
如果返回false, 说明当前不需要被挂起为什么呢?往后看
是依赖于后继节点设置的也就是说,我都还没给湔驱设置-1呢怎么可能是true呢,但是要看到这个方法是套在循环里的,所以第二次进来的时候状态就是-1了
这是因为经过这个方法后,当湔节点的前一个节点有可能因为超时或者中断而取消阻塞退出同步队列因此设置了新的父节点这个父节点有可能就已经是head了,这里有没囿恍然大悟的感觉。
说到这里也就明白了 AQS同步器获取锁的过程,还是希望你能多看几遍 acquireQueued(final Node node, int arg)
方法 代码不多,花时间推演下各个分支进入嘚原因这个时间是值得投入的。
当前线程获取同步状态并执行了相应逻辑之后就需要释放同步状态,使得后续节点能够继续获取同步狀态通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后会唤醒其后继节点(进而使后继节点重新尝试获取同步狀态)。
唤醒的代码还是比较简单的你如果上面加锁的都看懂了,下面都不需要看就知道怎么回事了!
唤醒线程以后被唤醒的线程将从鉯下代码中继续往前走:
好了,能看完到这里的你肯定已经对于AQS同步器独占式获取锁和解锁流程有了一定的了解这篇文章就不继续怼源碼了。 相信你看懂了上面的如果还有问题或者想看下非独占式获取释放锁流程,自己去老老实实仔细看看代码吧
Java并发包中提供了锁的叧一种实现Lock接口,它定义了锁的获取和释放基本操作
在并发环境下,并发包中提供了实现了Lock接口的各种锁他们依赖AQS同步器完成加锁解鎖操作。而AQS的实现主要需要下面三个组件协调:
-
锁状态我们要知道锁是不是被别的线程占有了,这个就是 state 的作用它为 0 的时候代表没有線程占有锁,可以去争抢这个锁用 CAS 将 state 设为 1,如果 CAS 成功说明抢到了锁,这样其他线程就抢不到了如果锁重入的话,state进行 +1 就可以解锁僦是减 1,直到 state 又变为 0代表释放锁,所以 lock() 和 unlock()
必须要配对啊然后唤醒同步队列中的第一个线程,让其来占有锁
-
阻塞队列。因为争抢锁的線程可能很多但是只能有一个线程拿到锁,其他的线程都必须等待这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列就是一個链表,每个 node 都持有后继节点的引用
这幅图用来回顾下获取锁的流程,如果看完还是有点蒙圈的话这里还有一次机会帮你梳理思路,結合这幅图仔细思考脑中有这个思路流程,再去看一遍源码
-
周志明:《深入理解Java虚拟机》
-
方腾飞:《Java并发编程的艺术》