开发者社区> wqnmbdd> 正文

ReentrantLock的lock-unlock流程详解

简介: 【本文转载自ReentrantLock的lock-unlock流程详解】 最近一段时间在研究jdk里的concurrent包,分为了线程管理,锁操作以及原子操作三个部分。线程管理平时用得还算多,但是锁操作和原子操作基本就没用过,只是以前在大学的时候跑了几个例子玩玩。当看到ReentrantLock的时候,发现用法倒是和synchronized有点类似也很简单,但是内部原理比较复杂。网上查
+关注继续查看

【本文转载自ReentrantLock的lock-unlock流程详解

最近一段时间在研究jdk里的concurrent包,分为了线程管理,锁操作以及原子操作三个部分。线程管理平时用得还算多,但是锁操作和原子操作基本就没用过,只是以前在大学的时候跑了几个例子玩玩。当看到ReentrantLock的时候,发现用法倒是和synchronized有点类似也很简单,但是内部原理比较复杂。网上查了关于ReentrantLock的相关内容,没发现有谁把它分析得很透彻,只是有几篇讲了内部的锁实现机制,只可惜都是以文字为主,难以把整个内部流程串起来理解,相信很多人没有真正把这个新的同步机制搞明白,当然也包括我在内。所以花了几天时间反复调试,终于把最基础的lock-unlock以及与ReentrantLock绑定的Condition的await-signal机制以流程图的形式画了出来,同时把我认为较为难理解的代码也分析了其内部原理加以注释,以此分享给大家,希望能帮助各位更快的理解它的实现原理,也欢迎批评指正。

       目前只是研究了正常状态下的同步原理,如果存在线程中断,其流程将更为复杂。所以暂时先上两篇无中断的流程讲解,待我能把带中断的流程整理清楚了再发出来,以免误导大家。

       在jdk1.5之前,多线程之间的同步是依靠synchronized来实现。synchronized是java的关键字,直接由jvm解释成为指令进行线程同步管理。因为操作简单,而且现在jdk的后续版本已经对synchronized进行了很多的优化,所以一直是大家编写多线程程序常用的同步工具。那为什么要推出新的同步api呢?jdk1.5发布的时候,synchronized性能并不好,这可能是concurrent包出现的一个潜在原因,但是更重要的是新的api提供了更灵活,更细粒度的同步操作,以满足不同的需求。但是问题也很明显,越灵活可控度越高就越容易出错,所以大多数人很少使用concurrent里的同步锁api。本文并不比较两者的优劣,毕竟存在即合理。能把更高级的工具用好,有时候提高工作效率,加快异常排查也是很不错的,不过本人是出于学习其原理及思想才进行研究,工作中用哪一个同样也会谨慎抉择。


ReentrantLock类图:



  • AbstractOwnableSynchronizer类保持和获取独占线程。
  • AbstractQueuedSynchronizer是以虚拟队列的方式管理线程的锁获取与锁释放,以及各种情况下的线程中断。提供了默认的同步实现,但是获取锁和释放锁的实现定义为抽象方法,由子类实现。目的是使开发人员可以自由定义获取锁以及释放锁的方式。
  • Sync是ReentrantLock的内部抽象类,实现了简单的获取锁和释放锁。
  • NonfairSync和FairSync分别表示“非公平锁”和“公平锁”,都继承于Sync,并且都是ReentrantLock的内部类。
  • ReentrantLock实现了Lock接口的lock-unlock方法,根据fair参数决定使用NonfairSync还是FairSync。
这里面有两个重点内容:
  1. AbstractQueuedSynchronizer内部的Node
  2. AbstractQueuedSynchronizer内部的state
Node:



       Node是对每一个访问同步代码的线程的封装。不仅包括了需要同步的线程,而且也包含了每个线程的状态,比如等待解除阻塞,等待条件唤醒,已经被取消等等。同时Node还关联了前驱和后继,即prev和next。个人认为是为了以集中的方式管理多个不同状态的线程,当不同的线程发生状态改变时,可以尽快的反应到别的线程上,提高运行效率。比如某个Node的prev已经被取消了,那么当对这个prev解除阻塞的时候就可以被忽略掉,进而尝试解除该Node的阻塞状态。

       多个Node连接起来成为了虚拟队列(因为不存在真正的队列容器将每个元素装起来所以说是虚拟的,我把它称为release队列,意思是等待释放),那么就得有head和tail。针对公平锁,head是不带线程的特殊Node,只有next,而最新一个请求锁的线程取锁失败时就把它添加到队尾,即tail。但是对于非公平锁,新请求锁的线程会插队,也许会插到最前面,也许不会。

       这里可能有人会有疑问:head放在队列中有什么用处?为什么不是一个等待锁的线程作为head呢?原因很简单,因为每个等待线程都有可能被中断而取消,对于一个已经取消的线程,自然是有机会就把它gc了。那么gc前一定得让后续的Node成为head,这样一来setHead的操作过于分散,而且要应对多种线程状态的变化来设置head,这样就太麻烦了。所以这里很巧妙地将head的next设置为等待锁的Node,head就相当于一个引导的作用,因为head没有线程,所以不存在“取消”这种状态。


State:

                       

state是用来记录锁的持有情况。

  • 没有线程持有锁的时候,state为0。
  • 当某个线程获取锁时,state的值增加,具体增加多少开发人员可自定义,默认为1,表示该锁正在被一个线程占有。
  • 当某个已经占用锁的线程再次获取到锁时,state再增长,此为重入锁。
  • 当占有锁的线程释放锁时,state也要减去当初占有时传入的值,默认为1。
多个线程竞争锁的时候,state必须通过CAS进行设置,这样才能保证锁只能有一个线程持有。当然这是排它锁的规则,共享锁就不是这样了。

公平锁和非公平锁的不同仅在于修改state的时机。看下面的代码就能明白:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // ReentrantLock.class  
  2.   
  3. protected final boolean tryAcquire(int acquires) {  
  4.     final Thread current = Thread.currentThread();  
  5.     int c = getState();  
  6.     if (c == 0) {  
  7.         // 此为公平锁的实现,而非公平锁不调用hasQueuedPredecessors方法,即不需要判断队列里是否有内容,直接通过CAS修改state来竞争锁  
  8.         if (!hasQueuedPredecessors() &&  
  9.             compareAndSetState(0, acquires)) {  
  10.             setExclusiveOwnerThread(current);  
  11.             return true;  
  12.         }  
  13.     }  
  14.     else if (current == getExclusiveOwnerThread()) {  
  15.         int nextc = c + acquires;  
  16.         if (nextc < 0)  
  17.             throw new Error("Maximum lock count exceeded");  
  18.         setState(nextc);  
  19.         return true;  
  20.     }  
  21.     return false;  
  22. }  

       基本上把Node和state的意义弄明白了,一个正常的lock-unlock过程应该很容易明白。但是代码里有很多地方要针对已取消的线程做特殊处理,所以理解上还是会有困难。因为已经有几篇文章进行了源码讲解,所以这里我就不再把每段源代码都拿出来细讲了。

先看看lock()的流程:(图中的Node0和Node1在源代码中不存在,是我为了方便说明清楚才添加的别称)


如图所示,彩色的字都是一个CAS操作,其中三个红色CAS都对成功和失败有相应地处理,为什么另外一个蓝色CAS不关心设置是否成功呢?下面这段代码里我给出了解释:
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // AbstractQueuedSynchronizer.class  
  2.   
  3. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
  4.     int ws = pred.waitStatus;  
  5.     if (ws == Node.SIGNAL)  
  6.         /* 
  7.          * This node has already set status asking a release 
  8.          * to signal it, so it can safely park. 
  9.          */  
  10.         return true;  
  11.     if (ws > 0) {  
  12.         /* 
  13.          * Predecessor was cancelled. Skip over predecessors and 
  14.          * indicate retry. 
  15.          */  
  16.         do {  
  17.             node.prev = pred = pred.prev;  
  18.         } while (pred.waitStatus > 0);  
  19.         pred.next = node;  
  20.     } else {  
  21.         /* 
  22.          * waitStatus must be 0 or PROPAGATE.  Indicate that we 
  23.          * need a signal, but don't park yet.  Caller will need to 
  24.          * retry to make sure it cannot acquire before parking. 
  25.          */  
  26.         /* 
  27.          * 为什么不关心是否成功却还要设置呢? 
  28.          * 
  29.          * 如果设置失败,表示前驱已经被signal了。如果前驱是head,说明有机会获取锁,所以返回false后还可以再次tryAcquire 
  30.          * 
  31.          * 如果设置成功,表示前驱等待signal。如果再次确认pred.waitStatus仍然是Node.SIGNAL,则表明前驱等待释放锁的情况下必须阻塞当前线程 
  32.          * 所以返回true后即被park 
  33.          */  
  34.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
  35.     }  
  36.     return false;  
  37. }  

再看下unlock()的流程图:


如图所示,这里的粉红色折线与lock流程图里的粉红色虚折线对应,即线程A调用lock阻塞与线程B调用unlock解除线程A的阻塞。同时可以看到unlock只有一个CAS操作,但是也不用关心设置是否成功。我给这段代码做了下面解释:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // AbstractQueuedSynchronizer.class  
  2.   
  3. private void unparkSuccessor(Node node) {  
  4.     /* 
  5.      * If status is negative (i.e., possibly needing signal) try 
  6.      * to clear in anticipation of signalling.  It is OK if this 
  7.      * fails or if status is changed by waiting thread. 
  8.      */  
  9.     int ws = node.waitStatus;  
  10.     /* 
  11.      * 为什么不关心是否成功却还要设置呢? 
  12.      * 
  13.      * 注意这里的Node实际就是head 
  14.      *  
  15.      * 如果设置成功,即head.waitStatus=0,则可以让这时即将被阻塞的线程有机会再次调用tryAcquire获取锁。 
  16.      * 也就是让shouldParkAfterFailedAcquire方法里的compareAndSetWaitStatus(pred, ws, Node.SIGNAL)执行失败返回false,这样就能再有机会再tryAcquire了 
  17.      * 
  18.      * 如果设置失败,新跟随在head后面的线程被阻塞,但是没关系,下面的代码会立即将这个阻塞线程释放掉 
  19.      */  
  20.     if (ws < 0)  
  21.         compareAndSetWaitStatus(node, ws, 0);  
  22.   
  23.     /* 
  24.      * Thread to unpark is held in successor, which is normally 
  25.      * just the next node.  But if cancelled or apparently null, 
  26.      * traverse backwards from tail to find the actual 
  27.      * non-cancelled successor. 
  28.      */  
  29.     Node s = node.next;  
  30.     if (s == null || s.waitStatus > 0) {  
  31.         s = null;  
  32.         for (Node t = tail; t != null && t != node; t = t.prev)  
  33.             if (t.waitStatus <= 0)  
  34.                 s = t;  
  35.     }  
  36.     if (s != null)  
  37.         LockSupport.unpark(s.thread);  
  38. }  
       以上的两段代码说明是我觉得比较难理解的,虽然有英文注释,但是却没说明为什么这么做,我也是反复调试才想明白。但是不得不说这两段代码的巧妙,尽可能利用CAS操作减少阻塞的机会,让线程能有更多机会获取锁,毕竟阻塞线程是内核操作,开销不小。

本篇只讲了普通的lock-unlock,下篇会讲讲等待-通知,即Condition的await-signal的详细流程


参考资料:

自旋锁、排队自旋锁、MCS锁、CLH锁        http://coderbee.net/index.php/concurrent/20131115/577/comment-page-1
深入JVM锁机制1-synchronized      http://blog.csdn.net/chen77716/article/details/6618779
深入JVM锁机制2-Lock      http://blog.csdn.net/chen77716/article/details/6641477
ReentrantLock和synchronized两种锁定机制的对比 http://blog.csdn.Net/fw0124/article/details/6672522
虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)  http://icyfenix.iteye.com/blog/1018932 可参考《深入理解Java虚拟机:JVM高级特性与最佳实践》


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,大概有三种登录方式:
9065 0
源码分析Retrofit请求流程
Retrofit 是 square 公司的另一款广泛流行的网络请求框架。前面的一篇文章《源码分析OKHttp执行过程》已经对 OkHttp 网络请求框架有一个大概的了解。今天同样地对 Retrofit 的源码进行走读,对其底层的实现逻辑做到心中有数。
937 0
《BREW进阶与精通——3G移动增值业务的运营、定制与开发》连载之63---BREW 应用的开发流程
版权声明:本文为半吊子子全栈工匠(wireless_com,同公众号)原创文章,未经允许不得转载。
651 0
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
19696 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
17986 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,阿里云优惠总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系.
24794 0
UDP socket流程(14)——ip_local_out及其调用的函数
作者:gfree.wind@gmail.com 博客:linuxfocus.blog.chinaunix.net ip_local_out的代码很短 int ip_local_out(struct sk_buff *skb) {     int err;     /*      调用netfilter的hook检查该包是否可以发送。
1257 0
+关注
106
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
OceanBase 入门到实战教程
立即下载
阿里云图数据库GDB,加速开启“图智”未来.ppt
立即下载
实时数仓Hologres技术实战一本通2.0版(下)
立即下载