ReentrantLock及AQS浅谈

一、AQS简介

AQS全称AbstractQueuedSynchronizer,是java并发包中的一个类,该类更像是一个框架,提供了一些模板方法供子类实现,从而实现了不同的同步器,如下图所示。ReentrantLock,ReentrantReadWriteLock,ThreadPoolExecutor这些常见类都使用了AQS。
 

 
以下是AQS的成员变量:
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
static final long spinForTimeoutThreshold = 1000L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
 
       看到这里大致能猜到AQS内部维护了一个双向链表,head,tail分别指向头尾,事实上,Node节点封装了尝试获取锁的线程对象,所有尝试获取锁的线程组成了一个链表,在公平锁情况下,例如ReentrantLock中的AQS子类FairSync,每次都是按照顺序头部节点先被唤醒并尝试获取锁。
        state 是同步状态位,具体是否能够获取锁就是通过修改state来实现,下面会有具体代码分析。
        spinForTimeoutThreshold相当于一个阈值,在一些提供等待时间的获取锁操作时,例如ReentrantLock. tryLock(long timeout, TimeUnit unit)方法,在判定是否需要阻塞线程时,如果时间小于spinForTimeoutThreshold,则不会被阻塞,用于快速响应一些等待时间很短的获取锁操作。
       其他成员变量是关于CAS操作的,AQS的很多操作都是基于CAS原子操作的,以确保线程安全。

二、ReentrantLock简介

ReentrantLock是根据AQS实现的独占锁,提供了两个构造方法,如下图所示:
 

 
ReentrantLock有三个内部类:Sync,NonfairSync,FairSync,继承关系如下:
 

 
ReentrantLock提供两种类型的锁:公平锁,非公平锁。分别对应FairSync,NonfairSync。默认实现是NonFairSync。
ReentrantLock提供了lock(),lockInterruptibly(),tryLock(),tryLock(long timeout, TimeUnit unit)四种获取锁的方式。

三、非公平锁lock源码分析

下面从ReentrantLock.lock()简述一下其源码实现
 

 
lock()方法内部是委托给sync变量来实现的,下面是NonfairSync的lock方法源码:
 

 
       在NonfairSync的lock方法体里,首先尝试修改state的值,上面说到,AQS很多操作都是基于CAS的,在这里,设置state的期望值是0(没有线程持有锁时的状态),修改值为1,如果成功,则返回true,并且设置持有锁的线程为当前线程。乐观情况下,lock方法获取锁操作到这里就结束了。
      但是,很多情况下并不是那么乐观,如果compareAndSetState操作失败,就会进入到acquire方法:
 

 
acquire方法在AQS类,这里,首先会调用tryAcquire方法,该方法的具体实现在NonfairSync中:

 
tryAcquire方法内部调用了Sync的nonfairTryAcquire方法:

 
       在Sync的nonfairTryAcquire方法体里,如果state为0,会再做一次compare and set操作,尝试修改state的值为1。
       如果state不为0,判断当前线程是否是持有独占锁的线程,如果是,将state值加上acquires(传入的是1),这里就是ReentrantLock可重入的内部实现。
       如果方法返回true,那么获取锁的操作结束,如果返回false,回到AQS的acquire()方法内部:

 
会继续调用方法addWaiter,返回结果后,执行 acquireQueued方法。下面看一下addWaiter方法:

 
       如上图所示,addWaiter方法的功能是将当前线程封装成Node,并加入到AQS链表尾部,参数mode是传入的Node.EXCLUSIVE,代表实例化的node是独占模式,而非共享模式,注意如果pred == null表示内部队列还没有初始化,则会调用enq(node)。或者pred != null 但是在compareAndSetTail失败时,也会调用enq(node)。例如,同时有多个线程node尝试加入到链表末尾,就会存在失败的可能。
进入到enq方法内部:

 
       这个方法的最外层是一个大的for循环,并且是一个死循环。出口返回条件只有一个:成功加入到链表的末尾。前面讲到,在链表为空或者添加node到链表末尾失败时会进入到enq方法,这里首先判断tail是否为空,如果为空,实例化一个空的Node节点,并且tail和head都指向这个空的Node,如果不为空,将node加入到链表末尾,如下图所示:
 

 
将node成功加入到链表中后,回到AQS的acquire()方法内部,开始执行acquireQueued方法:
 

 
       这里也是一个循环,循环体内首先获取node的前一个节点,即node.prev指向的节点p,接着判断p是否是head节点,如果是head节点,会尝试获取锁,tryAcquire方法在上面已经分析过。获取锁成功之后,sethead(node)会把node节点置为头节点,p.next = null将之前的head节点指向断掉,帮助jvm触发GC。最后返回当前线程在获取锁过程中是否曾经被中断。
       如果node.prev不是头节点,不会尝试获取锁,这也就是AQS内部链表的作用,会从链表的头部开始尝试获取锁,达到一个FIFO的作用。获取锁失败或者node.prev不是头节点,则会执行shouldParkAfterFailedAcquire:

 
       Node.SIGNAL说明该节点准备好被唤醒,若节点没有设置为该状态,线程不会阻塞。
shouldParkAfterFailedAcquire方法有三个作用:1、若pred.waitStatus状态位大于0,说明这个节点已经取消了获取锁的操作,doWhile循环会递归删除掉这些放弃获取锁的节点。2、若状态位不为Node.SIGNAL,且没有取消操作,则会尝试将状态位修改为Node.SIGNAL。3、状态位是Node.SIGNAL,表明线程是否已经准备好被阻塞并等待唤醒。
       最终,只有在pred.waitStatus已经等于Node.SIGNAL时才会返回true。其他情况返回false,然后acquireQueued会继续循环。
       在shouldParkAfterFailedAcquire返回true之后,acquireQueued方法体内继续执行parkAndCheckInterrupt():
 

该方法调用LockSupport.park()方法使线程阻塞。注意,ReentrantLock.lock()获取锁阻塞就是在这一步实现。阻塞的线程在其他线程释放锁之后会被LockSupport.unpark()唤醒。LockSupport.park(),LockSuppoert.unpark()最终都是调用了UNSAFE的native方法,这里不做分析。整个ReentrantLock.lock方法就分析到这里,下面看一下unlock操作。

四、非公平锁unlock源码分析

ReentrantLock.unlock()方法内部同样是交给sync的release实现:
 

 
sync.release()方法调用是在父类AQS中, release方法会先调用子类Sync的tryRelease()方法,如下图所示:
 

 
如下是子类Sync.tryRelease()的源码:
 

 
       首先获取state值,并减去releases,这里releases为1。若当前线程非独占锁拥有线程,抛出异常。若减去1后state为0,说明可以唤醒其他线程尝试获取锁,将free设置为true并返回,设置独占锁拥有者为null。
       如果不为0,设置state为减少后的值并且返回false,这样的话,就不会有后面唤醒其他线程的操作。所以,需要注意可重入的锁,在获取锁的时候,调用了多少次lock方法,释放锁时,就需要调用多少次unlock方法。
       在返回值为true之后,回到父类的release方法,最终会调用unparkSuccessor()方法:

 
       在unparkSuccessor方法中,会获取node.next,使变量s = node.next。若s不为空且状态位小于0,则满足唤醒条件,执行LockSupport.unpark()唤醒线程。否则执行for循环递归查找到离head最近的一个待唤醒节点唤醒,节点唤醒之后会继续执行获取锁的操作,上面已经做过分析,这里不做赘述。

五、其他

由于篇幅原因,只讨论了非公平锁的实现,在这里大概讲一下公平锁的“公平”体现在哪里,根据上面讲到的,非公平锁获取锁有两个地方:
1、在NonfairSync.lock方法体入口处就直接获取锁然后退出方法;
2、加入到链表中,每次链表头部的节点被唤醒,接着尝试获取锁。虽然头部节点被唤醒之后,会尝试获取锁,但可能会有线程在在NonfairSync.lock方法体入口处不进入链表就直接取得了锁。
       而在公平锁中,如下图所示,hasQueuedPredecessors会首先判断链表中是否有排队线程,没有排队线程才会尝试获取锁,否则加入到链表排队。严格保证了所有线程都是按照链表顺序先入先出的获取锁。
 

网易云捕-高效的APP质量跟踪平台

时间: 2024-10-19 11:58:00

ReentrantLock及AQS浅谈的相关文章

[Android] [Java] Process 创建+控制+分析 经验浅谈

无论是Android亦或者Java中或多或少需要调用底层的一些命令,执行一些参数: 此时我们需要用到Java的Process来创建一个子进程,之所以是子进程是因为此进程依赖于发起创建请求的进程,如果发起者被Kill那个子进程也将Kill. 对于Process相信使用过的朋友一定不会陌生,它具有如下特点: 1.创建简单 2.控制难 3.容易导致无法创建子进程 4.如果是多线程那么很有可能造成内存溢出 以上现象如果你只是偶尔使用一次,创建一个进程或许你什么都没有感觉到,但是如果你使用了多线程,进行了

.net中对象序列化技术浅谈

.net中对象序列化技术浅谈 2009-03-11 阅读2756评论2 序列化是将对象状态转换为可保持或传输的格式的过程.与序列化相对的是反序列化,它将流转换为对象.这两个过程结合起来,可以轻松地存储和传输数 据.例如,可以序列化一个对象,然后使用 HTTP 通过 Internet 在客户端和服务器之间传输该对象.反之,反序列化根据流重新构造对象.此外还可以将对象序列化后保存到本地,再次运行的时候可以从本地文件 中“恢复”对象到序列化之前的状态.在.net中有提供了几种序列化的方式:二进制序列化

浅谈——页面静态化

现在互联网发展越来越迅速,对网站的性能要求越来越高,也就是如何应对高并发量.像12306需要应付上亿人同时来抢票,淘宝双十一--所以,如何提高网站的性能,是做网站都需要考虑的. 首先网站性能优化的方面有很多:1,使用缓存,最传统的一级二级缓存:2,将服务和数据库分开,使用不同的服务器,分工更加明确,效率更加高:3,分布式,提供多台服务器,利用反向代理服务器nginx进行反向代理,将请求分散开来:4,数据库的读写分离,不同的数据库,将读操作和写操作分开,并实时同步即可:5,分布式缓存,使用memc

单页应用SEO浅谈

单页应用SEO浅谈 前言 单页应用(Single Page Application)越来越受web开发者欢迎,单页应用的体验可以模拟原生应用,一次开发,多端兼容.单页应用并不是一个全新发明的技术,而是随着互联网的发展,满足用户体验的一种综合技术. SEO 一直以来,搜索引擎优化(SEO)是开发者容易忽略的部分.SEO是针对搜索(Google.百度.雅虎搜索等)在技术细节上的优化,例如语义.搜索关键词与内容相关性.收录量.搜索排名等.SEO也是同行.市场竞争常用的的营销手段.Google.百度的搜

浅谈html标签

浅谈html各常用标签用法 标题标签:<h1>-<h6>来表示,使标题字体变粗. <br />换行标记 <hr />水平分隔符 &nbsp空格符 &copy版权符 <a href>a标签超链接 href可接链接地址 <p>段落标签<blockquote>引用标签及可用做缩进 <table>表格中的<ul>无序列表<ol>有序列表<dl>自定义列表<row

浅谈二维中的树状数组与线段树

一般来说,树状数组可以实现的东西线段树均可胜任,实际应用中也是如此.但是在二维中,线段树的操作变得太过复杂,更新子矩阵时第一维的lazy标记更是麻烦到不行. 但是树状数组在某些询问中又无法胜任,如最值等不符合区间减法的询问.此时就需要根据线段树与树状数组的优缺点来选择了. 做一下基本操作的对比,如下图. 因为线段树为自上向下更新,从而可以使用lazy标记使得矩阵的更新变的高校起来,几个不足就是代码长,代码长和代码长. 对于将将矩阵内元素变为某个值,因为树状数组自下向上更新,且要满足区间加法等限制

[nRF51822] 14、浅谈蓝牙低功耗(BLE)的几种常见的应用场景及架构(科普类干货)

蓝牙在短距离无线通信领域占据举足轻重的地位—— 从手机.平板.PC到车载设备, 到耳机.游戏手柄.音响.电视, 再到手环.电子秤.智能医疗器械(血糖仪.数字血压计.血气计.数字脉搏/心率监视器.数字体温计.耳温枪.皮肤水分计等), 再到智能家居等领域均占有一席之地. 而蓝牙低功耗(BLE)是在蓝牙4.0协议上修改以适用低功耗应用场景的一种蓝牙协议. 随着上一股智能消费类电子大潮的到来,BLE的各种应用也像雨后春笋般在市场上铺开. 如果想 紧跟蓝牙协议的最新动态 ,可以在https://www.b

浅谈C++容器动态内存管理的优化

在信息学竞赛中,C++的容器的用途非常广泛,但经常因常数过大而超时.怎样才能提高它们的效率呢? 我们知道,容器是存储同一类对象的对象,既然"对象"我们无法改变,那么我们只能从"存储"入手,不难想到,不同容器在实现上的根本区别是它们对应着不同的内存组织方式,内存管理无疑是这种实现的核心,所以优化内存管理是加快容器效率的最好途径之一. 一.内存分配器简介 怎样才能优化内存管理呢?很简单,C++为我们提供了这样的接口,我们可以通过自定义容器模板中的最后一个allocato

张小龙浅谈微信公众平台的意义

腾讯高级副总裁张小龙表示:微信公众平台,就是在移动互联网时代,让企业和个人以更简捷的形式提供服务给有需要的人. 张小龙浅谈微信公众平台的意义,布布扣,bubuko.com