您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
ReentrantLock 源码分析
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
ReentrantLock 源码分析
自猿其说Tech
2021-09-26
IP归属:未知
29960浏览
计算机编程
### 1 引子 ReentrantLock是Java并发包中提供的一个**可重入的互斥锁**。ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性。只不过相比原生的Synchronized,ReentrantLock增加了一些高级的扩展功能,比如它可以实现公平锁,同时也可以绑定多个Conditon。 ### 2 ReentrantLock特性 大家去火车站买票,先到的排队在前边,后到的排在后边,先到先得。这就是**公平锁**的基本思路。 大家嫌买票等待的时间长,就用自己的箱子占据位置,结果可能轮到某个人时,某个人不在位置,结果后边的人就先买到了票。这就是**非公平锁**的基本思路。 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是**可中断锁**。 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,但是B又不想无穷无尽的等待下去,只想等待一段时间,在这段时间内等待锁就使用,等不到就放弃,这种就是**锁限时**。 以上就是公平锁,非公平锁,锁中断,锁限时的大致概念,相信大家应该会有所理解。下面对ReentrantLock的特性进行详细的讲解。 ### 3 ReentrantLock非公平锁 ReentrantLock默认实现的是非公平锁,具体看一下代码: ```java ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); ``` 看一下构造函数: ```java public ReentrantLock() { sync = new NonfairSync(); } ``` 从这里可以看出ReentrantLock默认实现的是非公平锁,再看一下非公平锁是怎么具体实现的。 lock是一个接口,构造器默认实现的是NonfairSync,所reentrantLock.lock() 调用的是NonfairSync的lock接口。 看一下这块的具体代码: ```java static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } ``` NonfairSync 继承了Sync,Sync继承了AbstractQueuedSynchronizer,到这里大家应该已经明白ReentrantLock是基于AQS实现的,所以只要你搞懂AQS,很多并发类你都会很容易的理解。 reentrantLock.lock(),我们看一下lock方法做了哪些操作,首先通过CAS获取锁,如果获取到锁,把当前线程设置为独占线程。如果获取失败,则调用acquire方法,而此方法为AQS内部方法,此处不在详细的展开分析。 ```java public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } ``` 里有一个知识点,我们看一下NonfairSync类中tryAcquire方法,此方法也是AQS中的方法,也就是子类NonfairSync重写了父类AQS的tryAcquire方法。 当子类NonfairSync调用acquire方法的时候,执行的是AQS提供的acquire方法,然后从上面代码中可以看出来父类AQS在此方法中执行了tryAcquire方法,而tryAcquire方法在子类中已经重写,那么就会执行子类NonfairSync实现的tryAcquire方法。这就是多态。 这也是AQS的好处,对外提供API,子类继承AQS,按照自己的业务逻辑重写提供的API。而AQS只管线程怎么进行入队列,怎么插入节点,怎么唤醒节点这些底层的方法,对外层提供调用的 API,然后子类只需要继承AQS,实现独有的业务方法即可,从而大大降低了耦合度。 再看一下nonfairTryAcquire方法做了哪些操作: ```java final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前线程 int c = getState();//获取当前线程的状态 if (c == 0) {//如果当前线程处于初始状态 if (compareAndSetState(0, acquires)) {//cas竞争锁 setExclusiveOwnerThread(current);//竞争到锁把当前线程设置为独占线程 return true; } } else if (current == getExclusiveOwnerThread()) {//如果当前线程是独占线程,注意此方法也是实现重入的地方。 int nextc = c + acquires;//当前线程状态值+传入的值 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc);//设置最新的状态值 return true; } return false; } ``` 首先取到当前线程和当前线程的值,如果当前线程是初始状态那么就去竞争锁,如果竞争到锁,把当前线程设置为独占线程。如果进来的线程是独占线程,那么更新此线程进入的次数,同时也可以获取锁。如果没有竞争到锁,也不是当前的独占线程,那么就返回false。 从此方法中可以看出来,只要有线程进来,就让他获取锁,而不是排队到尾部。只要是独占线程,就可以重复进来,正是通过此方法可以看出来ReentrantLock是可以进行重入的也是可以是实现非公平锁的。 ### 4 ReentrantLock公平锁 再看一下ReentrantLock实现的公平锁的源代码: ```java ReentrantLock reentrantLock=new ReentrantLock(true); reentrantLock.lock(); ``` 点击构造器看一下: ```java public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } ``` 下面具体看一下FairSync类的源代码: ```java static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //没有线程在等队列里面等待同时获取到锁,则设置当前线程为独占线程 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } } ``` FairSync也是继承了Sync,也就是也是继承了AQS,调用lock方法,lock方法调用了父类acquire,此方法会调用子类重写的tryAcquire方法。它的实现方式和上面讲的非公平锁实现方式大致一样,业务逻辑都是在重写的tryAcquire里面。 看一下hasQueuedPredecessors: ```java public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } ``` 此方法主要是查看是否有线程在等待队列里面等待。 FairSync类tryAcquire方法业务逻辑就是获取到当前线程和当前线程的状态,如果当前线程是初始状态,会去判断当前队列里面是否有等待的线程,如果队列中没有等待的线程,同时获取到锁,那么就把当前线程设置为独占线程。如果是相同的独占线程进来,则更新独占线程进来的次数。同时返回true,否则返回false。从这里可以很容易的看出来,这是一个公平锁,进来的线程需要排队,队列中没有了线程才能轮到进来的线程。同时在else if 这个条件中可以看出来也是可重入的。 ### 5 ReentrantLock锁中断 看一下可中断锁的源代码: ```java ReentrantLock reentrantLock=new ReentrantLock(); reentrantLock.lockInterruptibly(); public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1);//调用AQS中方法 } ``` ```java public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted())//线程中断抛出异常 throw new InterruptedException(); if (!tryAcquire(arg))//此处还是调用创建对象子类的方法,获取不到锁执行下面的方法 doAcquireInterruptibly(arg); } ``` ```java 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);//如果线程抛出了异常,那么就把线程状态设置为取消状态同时清除节点. } } ``` 此处讲解一下,为什么ReentrantLock可以进行锁中断,为什么可以在产生死锁的时候,可以通过锁中断技术解决死锁。看过源码其实已经明白,首先在当前线程如果调用了interrupted,那么直接抛出异常出来。如果线程被阻塞并且被中断了那么也是抛出异常。也就是他是通过线程调用中断方法抛出异常来打破持有锁的。如果前面的文章看过,你会发在AQS中doAcquireInterruptibly方法和acquireQueued方法很相似,区别就是一个是返回boolean类型的值,让上层做判断,一个是在返回boolean类型值的地方直接抛出了异常。 ### 6 ReentrantLock锁限时 看一下可中断锁的源代码: ```java ReentrantLock reentrantLock=new ReentrantLock(); reentrantLock.tryLock(300, TimeUnit.SECONDS);//300秒内持续获取锁,直到获取到锁或者时间截止 ``` ```java public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted())//此处可以看出来支持锁中断 throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);//首先获取一次锁,如果没有获取到执行独占计时模式 } ``` ```java private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout <= 0L)//时间小于0直接返回 return false; final long deadline = System.nanoTime() + nanosTimeout;//队列延迟时间为系统时间+设置的超时时间 final Node node = addWaiter(Node.EXCLUSIVE);//把当前线程封装为Node并添加到队列 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 true; } nanosTimeout = deadline - System.nanoTime();//超时间为延迟时间-当前系统的时间 if (nanosTimeout <= 0L)//表示已经超过设置的尝试时间,直接返回 return false; if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)//如果当前线程阻塞,并且超时时间大于1000纳秒 LockSupport.parkNanos(this, nanosTimeout);//阻塞当前线程并在超时时间内返回 if (Thread.interrupted())//线程中断 throw new InterruptedException();//抛出异常 } } finally { if (failed)//线程发生异常 cancelAcquire(node);//把当前线程设置为取消状态并清除该节点 } } ``` 通过阅读源码我们发现锁限时获取的步骤: 1. 首先调用tryAcquire方法获取一次锁,如果没有获取到调用AQS中的doAcquireNanos。 1. System.nanoTime() 获取系统纳秒级时间+传递的延时时间为最后的时间。 1. 调用addWaiter方法把当前线程封装为节点并添加到队列的尾部。 1. 前驱节点是头节点并且获取到锁设置当前节点为头节点 1. 如果当前线程被阻塞了并且超时时间大于1000纳秒,调用LockSupport.parkNanos方法阻塞当前线程并且在规定的超时间内返回 1. 如果线程中断,直接抛出异常,这里可以看出支持锁中断 1. 如果线程在自旋的过程中发生了异常,那么调用cancelAcquire方法把当前线程设置为取消状态并且清除该节点。 其实此方法和上一篇讲解的独占锁模式调用acquireQueued方法差不多。不同点在于这里增加了超时时间,如果超时时间大于spinForTimeoutThreshold,此值是一个常量为1000的值。也就是如果超时时间大于1000纳秒,那么就调用 LockSupport.parkNanos方法让该线程阻塞,最长阻塞的时间不会超过超时的时间。同时增加了线程中断的判断,发生线程中断则抛出异常,其余和acquireQueued实现都一样。 ### 7 总结 分析了ReentrantLock源码实则就是在分析AQS同步器源码,包括Java.util.Concureent并发包下的大部分工具也是基于这个同步器实现的。 ReentrantLock锁的实现步骤可以理解为如下: 1. 未竞争到锁的线程将会被CAS为一个链表结构并且被挂起。 1. 竞争到锁的线程执行完后释放锁并且将唤醒链表中的下一个节点。 1. 被唤醒的节点将从被挂起的地方继续执行逻辑。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:客户服务技术部 乔杰
原创文章,需联系作者,授权转载
上一篇:通过keepalived保证nginx高可用
下一篇:鸿蒙App-开发从环境搭建到“Hello Harmony”
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号