一、ReentranLock

  相信我们都使用过ReentranLock,ReentranLock是Concurrent包下一个用于实现并发的工具类(ReentrantReadWriteLock、Semaphore、CountDownLatch等),它和Synchronized一样都是独占锁,它们两个锁的比较如下: 
  1. ReentrantLock实现了Lock接口,提供了与synchronized同样的互斥性和可见性,也同样提供了可重入性。 
  2. synchronized存在一些功能限制:无法中断一个正在等待获取锁的线程,无法获取一个锁时无限得等待下去。ReentrantLock更加灵活,能提供更好的活跃性和性能,可以中断线程 
  3. 内置锁的释放时自动的,而ReentrantLock的释放必须在finally手动释放 
  4. 在大并发量的时候,ReentranLock的效率会比Synchronized好很多 
  5. Lock可以进行可中断的(lock.lockInterruptibly())、可超时的(tryLock(long time, TimeUnit unit))、非阻塞(tryLock())的方式获取锁 
  更多关于Lock和synchronized:Java并发编程:Lock

  一个并发工具自然最基本的功能就是获取锁和释放锁,那么有没有想过,ReentranLock是如何来实现并发的?既然ReentranLock可以中断线程,所以内部自然不可能使用synchronized来实现。事实上,ReentranLock只是一个工具类,它内部的的实现都是通过一个AbstractQueuedSynchronizer(简称AQS)来实现的,AQS是整个Concurrent包中最核心的地方,其它的并发工具也都是使用AQS来实现的,因此,以下我们就通过ReentranLock来分析AQS是如何实现的!

二、AQS

  站在使用者的角度,AQS的功能可以分为两类:独占功能和共享功能,它的所有子类中,要么实现并使用了它独占功能的API,要么使用了共享锁的功能,而不会同时使用两套API,即便是它最有名的子类ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套API来实现的,为什么这么做,后面我们再分析,到目前为止,我们只需要明白AQS在功能上有独占控制和共享控制两种功能即可 
  AQS类中,有一个叫做state的成员变量,在ReentranLock他表示获取锁的线程数,假如state=0,表示还没有现成获取锁;1表示已经有现成获取了锁;大于1表示重入的数量

三、ReentranLock的源码

  首先我们要对ReentranLock有一个大体的了解,ReentranLock分为公平锁和非公平锁,并且ReentranLock是AQS独占功能的体现 
  公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁,就像排队一样 
  非公平锁:表示获取锁的线程是不定顺序的,谁运气好,谁就获取到锁 
  
  
  可以看到,两个锁都是继承了一个叫做Sync的类,并且都分别有两个方法lock和tryAcquire,那我们看看Sync这个类: 
  
  原来,Sync继承自AQS,并且公平锁和非公平锁的两个方法lock和tryAcquire都是重写了Sync的方法,这也就验证了ReentrantLock的实现原理就是AQS

  到这里,我们已经有了基本的认识,那么我们就想想,公平锁和非公平锁该如何实现: 
  有那么一个被volatile修饰的标志位叫做key(其实就是上面所说的AQS中的state),用来表示有没有线程拿走了锁,还需要一个线程安全的队列,维护一堆被挂起的线程,以至于当锁被归还时,能通知到这些被挂起的线程,可以来竞争获取锁了。 
  因此,公平锁和非公平锁唯一的区别就是获取锁的时候,是先直接去获取锁还是先进入队列中等待

四、ReentranLock的加锁

  我们来看看ReentranLock是如何加锁的:

公平锁

  
  公平锁调用lock时,会直接调用父类AQS的acquire方法,这里传入1,很简单,就是告知有一个线程要获取锁,这里是定死的;因此,相反,在释放锁的时候,也是传入1 
   
  在acquire中,首先调用tryAcquire,目的尝试获取锁,如果获取不到,就调用addWaiter创建一个waiter(当前线程)防止到队列中,然后自身阻塞,那我们来看看如何尝试获取锁?(注意:两个锁都重写了AQS的tryAcquire方法)

        protected final boolean tryAcquire(int acquires) {//首先得到获取锁的当前线程final Thread current = Thread.currentThread();//获取当前stateint c = getState();//如果当前没有线程获取锁if (c == 0) {//hasQueuedPredecessors表示当前队列是否有线程在等待//表示没有线程在等待,同时采用CAS更新state的状态if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {//然后设置一个属性exclusiveOwnerThread = current,记录锁被当前线程拿去setExclusiveOwnerThread(current);return true;}}//如果c != 0,说明已经有线程获取锁,并且getExclusiveOwnerThread == current,表示当前正在获取锁的就是当前锁,所以这里是重入!else if (current == getExclusiveOwnerThread()) {//重入的话,让状态为state+1,表示多一次重入int nextc = c + acquires;//如果当前状态<0,说明出现异常if (nextc < 0)throw new Error("Maximum lock count exceeded");//设置当前标志位setState(nextc);return true;}//如果锁已经被获取,并且又不是重入,所以返回false,表明获取锁失败return false;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

  获取锁的逻辑上面说得很明白了,但是这里需要了解的是CAS操作和队列的数据结构,这个下面在说,我们接着看,回到tryAcquire中 
   
  如果获取锁成功,则不操作;如果获取锁失败,则调用addWaiter并采取Node.EXECLUSIVE模式把当前线程放到队列中去,mode是一个表示Node类型的字段,仅仅表示这个节点是独占的,还是共享的

    private Node addWaiter(Node mode) {//把当前线程按照Node.EXECLUSIVE模式包装成1个NodeNode node = new Node(Thread.currentThread(), mode);//用pred表示队列中的尾节点Node pred = tail;//如果尾节点不为空if (pred != null) {node.prev = pred;//通过CAS操作把node插入到列表的尾部,并把尾节点指向node如果失败,说明有并发,此时调用enqif (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//如果队列为空,或者CAS失败,进入enq中死循环,“自旋”方式修改。enq(node);return node;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

  先看下AQS中队列的内存结构,我们知道,队列由Node类型的节点组成,其中至少有两个变量,一个封装线程,一个封装节点类型。 
  而实际上,它的内存结构是这样的(第一次节点插入时,第一个节点是一个空节点,代表有一个线程已经获取锁,事实上,队列的第一个节点就是代表持有锁的节点): 
  

    private Node enq(final Node node) {//进入死循环for (;;) {Node t = tail;//如果尾节点为null,说明队列为空if (t == null) {//此时通过CAS增加一个头结点(即上图的黄色节点),并且tail也指向头结点,之后下一次循环if (compareAndSetHead(new Node()))tail = head;} else {//否则,把当前线程的node插入到尾节点的后面node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;//并返回插入结点的前一个节点return t;}}}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

  这就完成了线程节点的插入,还需要做一件事:将当前线程挂起!,这里在acquireQueued内通过parkAndCheckInterrupt将线程挂起

   final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//如果当前的节点是head说明他是队列中第一个“有效的”节点,因此尝试获取,if (p == head && tryAcquire(arg)) {//成功后,将上图中的黄色节点移除,Node1变成头节点。setHead(node);p.next = null; // help GCfailed = false;//返回true表示已经插入到队列中,且已经做好了挂起的准备return interrupted;}//否则,检查前一个节点的状态为,看当前获取锁失败的线程是否需要挂起。如果需要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。知道被唤醒。if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;}} finally {if (failed) //如果有异常cancelAcquire(node);// 取消请求,对应到队列操作,就是将当前节点从队列中移除。}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

  这块代码有几点需要说明:

  1. Node节点中,除了存储当前线程,节点类型,队列中前后元素的变量,还有一个叫waitStatus的变量,改变量用于描述节点的状态,为什么需要这个状态呢? 
 
  原因是:AQS的队列中,在有并发时,肯定会存取一定数量的节点,每个节点[G4] 代表了一个线程的状态,有的线程可能“等不及”获取锁了,需要放弃竞争,退出队列,有的线程在等待一些条件满足,满足后才恢复执行(这里的描述很像某个J.U.C包下的工具类,ReentrankLock的Condition,事实上,Condition同样也是AQS的子类)等等,总之,各个线程有各个线程的状态,但总需要一个变量来描述它,这个变量就叫waitStatus,它有四种状态: 
  节点取消 
  节点等待触发 
  节点等待条件 
  节点状态需要向后传播。 
  只有当前节点的前一个节点为SIGNAL时,才能当前节点才能被挂起。

  2. 对线程的挂起及唤醒操作是通过使用UNSAFE类调用JNI方法实现的。当然,还提供了挂起指定时间后唤醒的API,在后面我们会讲到。 
   
  (这一块分析来自:http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer#anch140431)

  我们来理一理思路: 
  1. 调用lock方法获取锁,而lock方法内值调用了AQS的acquire(1) 
  2. 然后尝试获取锁,如果当前state标志==0,表示还没有线程获取锁,然后再判断是否有队列在等待获取该锁,如果没有队列,说明当前线程是第一个获取该锁的线程,然后修改标志位,并且用一个变量exclusiveOwnerThread来记录当前线程获取了锁 
  3. 如果是重入状态,也修改state+1 
  4. 如果锁已被占取,获取失败 
  5. 如果获取失败,则把当前线程包装成一个Node,插入到队列中, 
  6. 否则,检查前一个节点的状态为,看当前获取锁失败的线程是否需要挂起。如果需要,借助JUC包下的LockSopport类的静态方法Park挂起当前线程。知道被唤醒。

非公平锁


  这里可以看到,非公平锁,首先是直接去获取锁,如果有并发获取失败,调用AQS的acquire(1),然后acquire中调用非公平锁的tryAcquire,进而调用nonfairTryAcquire

    final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();//如果当前没有现成获取锁,直接获取锁,然后设置一个属性exclusiveOwnerThread = current,记录锁被当前线程拿去,这里和公平所有细微的差别,公平所还要判断hasQueuedPredecessors()if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}//如果是重入else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}//如果当前锁获取失败,返回falsereturn false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

  其它的都和公平锁一样了,如果到这里都获取失败了,就会插入到队列中阻塞起来

总结公平锁和非公平锁

  1. 公平锁获取锁时,会老老实实得走AQS的流程去获取锁
  2. 非公平锁获取锁是,首先会抢占锁,达到不排队的目的,如果抢占失败,只能老老实实排队了

五、ReentrantLock的释放锁

  从上面我们可以知道,当锁已被占,获取锁的线程会一直在队列中排队(FIFO),那么我们想想,释放的时候该怎么做? 
  1. 首先锁的状态位要改变 
  2. 队列中的头结点去获取锁

  我们来看看代码验证一下: 
  释放锁的时候调用unlock(),然后在方法中调用AQS的release方法 
   
   
  在release方法中,首先调用tryRelease方法,由于继承自AQS的Sync类重写了tryRelease方法,所以此时执行的是Sync的tryRelease方法

        protected final boolean tryRelease(int releases) {//这里传入的releases是1,跟获取锁时传入的1一致,更新state状态int c = getState() - releases;//如果当前占领锁的线程不是尝试释放锁的线程,会抛出非法异常if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;//如果释放成功,则修改获取锁的变量为null,但是因为是重入的关系,不是每次释放锁c都等于0,直到最后一次释放锁时,才通知AQS不需要再记录哪个线程正在获取锁if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

  此时已经释放了锁,然后便通知队列头部的线程去获取锁 
   
  寻找的顺序是从队列尾部开始往前去找的最前面的一个waitStatus小于0的节点,找到这个及节点后,利用LockSopport类将其唤醒,这个waitStatu前面说过了,不记得了到前面看看。 
  

六、总结

  在Concurrent包中,基本上并发工具都是使用了AQS作为核心,因此AQS也是并发编程中最重要的地方!我们从ReentrantLock出发,去探讨了AQS的实现原理,其实并不难,AQS中采用了一个state的状态位+一个FIFO的队列的方式,记录了锁的获取,释放等,这个state不一定用来代指锁,ReentrantLock用它来表示线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask用它来表示任务的状态(尚未开始,正在运行,已完成以及以取消)。同时,在AQS中也看到了很多CAS的操作。AQS有两个功能:独占功能和共享功能,而ReentranLock就是AQS独占功能的体现,而CountDownLatch则是共享功能的体现

深入分析AbstractQueuedSynchronizer独占锁的实现原理:ReentranLock相关推荐

  1. 多线程—AQS独占锁与共享锁原理

    java.util.concurrent.locks包下,包含了多种锁,ReentrantLock独占锁.ReentrantReadWriteLock读写锁等,还有java.util.concurre ...

  2. Java-Lock独占锁与共享锁原理

    个人理解记录 ReentrantLock基于aqs实现,他的基本原理是aqs的status为0时表示锁被占用,为1时表示锁被释放.ReentrantLock在使用时需要显式的获取和释放锁,一般用try ...

  3. 深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/75043422 出自[zejian ...

  4. 转载:Lock锁机制的原理及与Synchronized的比较——源码解说

    文章转载自:https://blog.csdn.net/Luxia_24/article/details/52403033(为了简化阅读难度,本文只挑选了大部分内容进行转载,并对代码进行了注释,更加详 ...

  5. Java Review - 并发编程_独占锁ReentrantLock原理源码剖析

    文章目录 Synchronized vs ReentrantLock ReentrantLock概述 获取锁 void lock() 非公平锁的实现代码 非公平锁是如何体现的? 公平锁是怎么实现公平的 ...

  6. mysql锁机制为何设计如此复杂_再谈mysql锁机制及原理—锁的诠释

    加锁是实现数据库并发控制的一个非常重要的技术.当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁.加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更 ...

  7. 【MySQL】 ---- 共享锁、独占锁、行锁、表锁

    锁 1.一致性读(Consitent Reads) 2.锁定读(Locking Reads) 2.1 共享锁 2.2 独占锁 2.2 锁定读语句 3.行锁 3.1 行锁 3.1.1 Record Lo ...

  8. 【MySQL进阶】MySQL事务隔离与锁机制底层原理万字总结(建议收藏!!)

    [MySQL进阶]MySQL事务隔离与锁机制底层原理万字总结(建议收藏!!) 参考资料: 美团技术团队:Innodb中事务隔离级别和锁的关系 数据库的锁,到底锁的是什么? 阿里面试:说说一致性读实现原 ...

  9. 【线程、锁】什么是AQS(锁分类:自旋锁、共享锁、独占锁、读写锁)

    文章目录 1. 什么是AQS 1.1 锁分类 1.2 具体实现 2. AQS底层使用了模板方法模式 3. AQS的简单应用 参考 1. 什么是AQS AQS:全称为AbstractQuenedSync ...

最新文章

  1. 漂浮机器人新进展:Cimon的头部将为国际空间站带来人工智能
  2. 利用jdom生成XML文件
  3. minicom的使用,发送AT指令
  4. 最全mysql的复制和读写分离
  5. java api集合,javaAPI_集合基础_集合中常见操作示例
  6. [Android Studio] Android Studio如何提示函数用法
  7. jsoup 去除html标签,如何使用jsoup取消注释html标签
  8. IOS 公共类-数字处理
  9. pingback协议与traceback协议的区别
  10. 六、Linux常用命令——压缩解压缩命令
  11. python123平台第三周作业答案_python123第一周作业
  12. text 热敏打印机_GitHub - huangzhiyi/thermal_printer: Java实现网络小票打印机自定义无驱打印...
  13. 西南大学计算机科学学院官网,西南大学计算机与信息科学学院研究生导师简介-李艳涛...
  14. knx智能照明控制系统电路图_智能照明KNX灯控软件
  15. 编程题 java 密码锁_Java实现 蓝桥杯VIP 算法提高 密码锁
  16. C4996 scanf:This function or variable may be unsafe. / C6031 返回值被忽略.
  17. CodeForces 858C Did you mean... 、 CodeForces 858D Polycarp's phone book!黑科技
  18. 7_22_html_美食网设计
  19. Jbuilder2005破解补丁使用方法和下载地址
  20. 科大讯飞语音实现Android拨号之一

热门文章

  1. Linux虚拟机示范
  2. 华为oj题java单词博弈_【华为OJ】201301 JAVA 题目0-1级 将数组分为相等的两组
  3. aes 加密_Jmeter处理AES加密接口
  4. sql的limit用法
  5. 一维数据高斯滤波器_透彻理解高斯混合模型
  6. java 原理图_Java中比较重要的原理图(三大框架、、、、)
  7. pypark hive 开启动态分区_Hive分区与分桶
  8. .bin 文件用excel文件打开_bin文件怎么打开呢?
  9. html上滑效果,上滑菜单定位.html
  10. java泛型(三)、通配符的使用