一、简介

1.1 数据结构

ReetrantLock的底层是借助AbstractQueuedSynchronized实现,所以其数据结构依附于AbstractQueuedSynchronized的数据结构。

1.2 继承关系

public class ReentrantLock implements Lock, java.io.Serializable

ReetranLock实现了Lock接口,Lock接口中定义了lock与unlock相关操作,并且还存在newCondition方法,表示生成一个条件。

1.3 原理

ReentrantLock主要利用CAS+AQS队列来实现,它支持公平所和非公平锁,两者的实现类似:

先通过CAS尝试获取锁,如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候:

  • 非公平锁:如果同时还有另一个线程来尝试获取,那么有可能会让这个线程抢先获取
  • 公平锁:如果同时还有另一个线程来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

1.4 AQS

AQS是一个用于构建锁和同步容器的框架。concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semphore,CountDownLatch等,AQS解决了在实现同步容器时设计的大量细节问题。

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称为”哨兵节点“或”哑节点“,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。

1.5 使用示例

private Lock lock = new ReentrantLock();public void test(){lock.lock();try{doSomeThing();}catch (Exception e){// ignored}finally {lock.unlock();}
}

二、源码分析

2.1 内部类

ReetrantLock总共有三个内部类,并且三个内部类是紧密相关的,三个类的关系如下:

说明:ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。

2.2 Sync类

     //使用ASQ state来指示这个锁被持有了多少次abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = -5179523762034025860L;//获取锁abstract void lock();//非公平方式获取final boolean nonfairTryAcquire(int acquires) {//当前线程final Thread current = Thread.currentThread();//获取状态int c = getState();if (c == 0) {//表示没有线程正在竞争该锁//CAS设置成功,状态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;}return false;}//试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它protected final boolean tryRelease(int releases) {int c = getState() - releases;//当前线程不为独占线程if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}//判断资源是否被当前线程占有protected final boolean isHeldExclusively() {// While we must in general read state before owner,// we don't need to do so to check if current thread is ownerreturn getExclusiveOwnerThread() == Thread.currentThread();}final ConditionObject newCondition() {return new ConditionObject();}// Methods relayed from outer class//返回资源的占用线程final Thread getOwner() {return getState() == 0 ? null : getExclusiveOwnerThread();}final int getHoldCount() {return isHeldExclusively() ? getState() : 0;}final boolean isLocked() {return getState() != 0;}/*** Reconstitutes the instance from a stream (that is, deserializes it).*/private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();setState(0); // reset to unlocked state}}

Sync类方法和作用如下:

方法 作用
lock 锁定,并未实现,留给具体子类实现
nonfairTryAcquire 非公平方式获取
tryRelease 试图在共享模式下获取对象状态,此方法应该查询是否允许它在共享模式下获取对象状态,如果允许,则获取它
isHeldExclusively 判断资源是否被当前线程占有
newCondition 新生一个条件
getOwner 返回占有资源的线程
getHoldCount 返回状态
isLocked 资源是否被占用
readObject 自定义反序列化逻辑

2.3 NonfairSync类

NonfairSync类继承了Sync类,采用非公平策略获取锁,其实现了Sync类中抽象的lock方法,具体实现如下:

首先用一个CAS操作,判断state是否为0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的线程就进入等待队列。

“非公平性”也就体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队“了

     // 获得锁final void lock() {if (compareAndSetState(0, 1)) // 把当前线程设置独占锁setExclusiveOwnerThread(Thread.currentThread());else // 锁已经被占用,或者set失败// 以独占模式获取对象,忽略中断acquire(1); }

若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁返回,那么线程B和C则设置state失败,进入else,执行acquire方法

2.3.1 acquire在AQS中实现

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

2.3.1.1 acquire第一步:尝试获取锁,如果获取锁成功,方法直接返回

tryAcquire的实现

protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}

这里调用了nonfairTryAcquire

nonfairTryAcquire的实现

整个流程是:
检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数,如果以上两点都没有成功,则获取锁失败,返回false

/*** Performs non-fair tryLock.  tryAcquire is implemented in* subclasses, but both need nonfair try for trylock method.*/final boolean nonfairTryAcquire(int acquires) {//获取当前线程final Thread current = Thread.currentThread();//获取state变量int c = getState();//没有线程占用锁的情况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");//更新state值为新的重入次数setState(nextc);return true;}//获取锁失败return false;}

2.3.1.2 acquire第二步:入队

由上面可知,线程A已经占用了锁,所以B和C执行tryAcquire失败,并且进入等待队列。

addWaiter实现:

addWaiter函数是在AQS中实现的

//将新节点和当前线程关联并且入队列
private Node addWaiter(Node mode) {//初始化节点,设置关联线程和模式(独占or共享)Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;//尾节点不为空,说明队列已经初始化过if (pred != null) {node.prev = pred;//设置新节点为尾节点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//尾节点为空,说明队列还未初始化,需要初始化head节点并入对新节点enq(node);return node;}

此时B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程走到enq(node)。我们假设这两个线程同时走到了enq(node)里。

2.1.2.5 enq

这里体现了经典的自旋+CAS操作来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队

private Node enq(final Node node) {//开始自旋for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

当B、C入队后,此时AQS队列如下:

2.3.1.2 acquire第二步:挂起

此时B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起

acquireQueued实现:

//已经入队的线程尝试获取锁
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)) {//获取成功,将当前结点设置为head结点setHead(node);//原head结点出队,在某个时间点被GC回收p.next = null; // help GC//获取成功failed = false;//返回是否被中断过return interrupted;}//判断获取失败后是否可以挂起,若可以挂起if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//线程若被中断,设置interrupted为trueinterrupted = true;}} finally {if (failed)cancelAcquire(node);}}

假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第二个if语句中。接下来看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事

shouldParkAfterFailedAcquire实现:

//判断当前线程获取锁失败之后是否需要挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//前驱结点的状态int ws = pred.waitStatus;//前驱结点状态为signal,返回trueif (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;//前驱结点状态为CANCELLEDif (ws > 0) {//从队尾向前寻找第一个状态不为CANCELLED的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*///将前驱结点的状态设置为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

parkAndCheckInterrupt实现:

//挂起当前线程,返回线程中断状态并重置
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

线程入队后能够挂起的前提是,它的前驱结点的状态为SIGNAL,它的含义是”Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!“。所以shouldParkAfterFailedAcquired会先判断当前结点的前驱结点是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱结点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱结点的状态设置为SIGNAL。

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去竞争锁。

最终队列可能如下图所示:

2.3.2 unlock()

public void unlock() {sync.release(1);}
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

解锁的过程相比加锁容易很多。流程大致为先尝试释放锁,若释放成功,那么查看头节点的状态是否为SIGNAL,如果是则唤醒头结点的下个结点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们可以发现,每次都只唤起头节点的下一个结点关联的线程。

最后看下tryRelease的执行过程:

protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}

这里入参为1,tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常,若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且清空独占线程,最后更新state值,返回free

以下用一张流程图总结了非公平锁的获取锁的过程

2.4 FairSyn类

公平锁和非公平锁的不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行acquire(1)

// 公平锁static final class FairSync extends Sync {// 版本序列化private static final long serialVersionUID = -3000897897090466540L;final void lock() {// 以独占模式获取对象,忽略中断acquire(1);}// 尝试公平获取锁protected final boolean tryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取状态int c = getState();if (c == 0) { // 状态为0if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) { // 不存在已经等待更久的线程并且比较并且设置状态成功// 设置当前线程独占setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) { // 状态不为0,即资源已经被线程占据// 下一个状态int nextc = c + acquires;if (nextc < 0) // 超过了int的表示范围throw new Error("Maximum lock count exceeded");// 设置状态setState(nextc);return true;}return false;}}

跟踪lock方法的源码可知,当资源空闲时,它总是会先判断sync队列(AbstractQueuedSynchronizer中的数据结构)是否有等待时间更长的线程,如果存在,则将该线程加入到等待队列的尾部,实现了公平获取原则。其中,FairSync类的lock的方法调用如下,只给出了主要的方法。

可以看出只要资源被其他线程占用,该线程就会添加到sync queue中的尾部,而不会先尝试获取资源。这也是和Nonfair最大的区别,Nonfair每一次都会尝试去获取资源,如果此时该资源恰好被释放,则会被当前线程获取,这就造成了不公平的现象,当获取不成功,再加入队列尾部。

三、使用实例

3.1 公平锁

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class test {public static void main(String[] args) throws Exception {Lock lock=new ReentrantLock(true);MyThread t1=new MyThread("t1",lock);MyThread t2=new MyThread("t2",lock);MyThread t3=new MyThread("t3",lock);t1.start();t2.start();t3.start();}
}class MyThread extends Thread{private Lock lock;public MyThread(String name,Lock lock){super(name);this.lock=lock;}public void run(){lock.lock();try{System.out.println(Thread.currentThread()+" running");try{Thread.sleep(500);}catch (InterruptedException e) {e.printStackTrace();}}finally {lock.unlock();}}}

运行结果(某一次):

Thread[t1,5,main] running
Thread[t2,5,main] running
Thread[t3,5,main] running

该示例使用的是公平策略,由结果可知,可能回存在如下一种时序:

说明:首先,t1线程的lock操作 -> t2线程的lock操作 -> t3线程的lock操作 -> t1线程的unlock操作 -> t2线程的unlock操作 -> t3线程的unlock操作。根据这个时序图来进一步分析源码的工作流程。

  1. t1线程执行lock.lock,下图给出了方法调用中的主要方法

    说明:由调用流程可知,t1线程成功获取了资源,可以继续执行
  2. t2线程执行lock.lock,下图给出了方法调用中的主要方法

    说明:由上图可知,最后的结果是t2线程会被禁止,因为调用了LockSupport.park
  3. t3线程执行lock.lock,下图给出了方法调用中的主要方法
  4. t1线程调用了lock.unlock,下图给出了方法调用中的主要方法

    说明:如上图所示,最后,head的状态会变为0,t2线程会被unpark,即t2线程可以继续运行。此时t3线程还是被禁止。
  5. t2获得CPU资源,继续运行,由于t2之前被park了,现在需要恢复之前的状态,下图给出了方法调用中的主要方法

    说明:在setHead函数中会将head设置为之前head的下一个结点,并且将pre域与thread域都设置为null,在acquireQueued返回之前,sync queue就只有两个结点了。
  6. t2执行lock.unlock,下图给出了方法调用中的主要方法。


说明:由上图可知,最终unpark t3线程,让t3线程可以继续运行。
7. t3线程获取cpu资源,恢复之前的状态,继续运行。

8. t3执行lock.unlock,下图给出了方法调用中的主要方法

说明:最后的状态和之前的状态是一样的,队列中有一个空节点,头结点为尾节点均指向它。

参考

ReentranLock源码分析相关推荐

  1. Java并发之AQS源码分析ReentranLock、ReentrantReadWriteLock、Condition

    基于AQS的独享锁和共享锁的源码分析 基本概念说明 锁的基本原理思考 测试环境 实现方案1 实现方案2 独占锁:ReentrantLock源码分析 类依赖和类成员变量说明 加锁过程,入口方法:lock ...

  2. ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述)

    - title: ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述) - date: 2021/8/16 文章目录 一.ReentrantLock 1. 构造函数 二.Reentr ...

  3. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  4. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  5. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  6. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  7. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  8. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  9. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

  10. MyBatis原理分析之四:一次SQL查询的源码分析

    上回我们讲到Mybatis加载相关的配置文件进行初始化,这回我们讲一下一次SQL查询怎么进行的. 准备工作 Mybatis完成一次SQL查询需要使用的代码如下: Java代码   String res ...

最新文章

  1. Sqli-labs less 50
  2. PLATFORM软件有限公司
  3. Eclipse 搭建Android开发环境(整理)
  4. Spring Boot Debug调试
  5. mysql点击计数器_MySql计数器,如网站点击数,如何实现高性能高并发的计数器功能...
  6. Hyper-V的安装准备
  7. 【CentOS 7架构16】,限制user_agent#171226
  8. axure 画小程序效果图_微信小程序-基于canvas画画涂鸦
  9. Linux 网络编程 TCP/UDP编程
  10. 成都公办计算机学校,成都计算机学校公立-成都公立的计算机学校有哪
  11. 手机java时代浏览器_巅峰之战 三款最热java手机浏览器横评
  12. Spring Security - 21 记住我功能
  13. DisparityCost Volume in Stereo
  14. 系统上线后如何确保原始数据的准确性?
  15. android开机卡logo,logcat怎么抓log
  16. 最佳的75个安全工具
  17. 微信小程序申请+开发+发布流程
  18. 威尔逊定理证明——杨子曰数学
  19. [SWPUCTF 2021 新生赛]babyrce
  20. 用calc()绘制手机图案解锁的九宫格样式

热门文章

  1. android textview设置大小,android – 如何缩放/调整文本大小以适应TextView?
  2. php中fread用法,php fread函数与fread函数用法
  3. Python数据分析、挖掘常用工具
  4. 输电线路巡检机器人PPT_国网泰安供电公司开展输电线路无人机精细化巡检
  5. 空间变量php,PHP名称空间可以包含变量吗?
  6. 图像分类python代码_20行代码:Serverless架构下用Python轻松搞定图像分类
  7. linux apache 停止命令,linux 下 apache启动、停止、重启命令
  8. java反射加载类_Java反射 - 动态类加载和重载
  9. ionic + angular + cordova, 打造专属自己的App!
  10. loadGrid layui