1. ReentrantLock,英文意思是可重入锁。从实际代码实现来说,ReentrantLock也是互斥锁(Node.EXCLUSIVE)。与互斥锁对应的的,还有共享锁Node.SHARED
  1. ReentrantLock 集成了Lock接口,Lock接口主要功能有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()。

    ReentrantLock有个内部的抽象类Sync,这个Sync类继承了AbstractQueuedSynchronizer类,内部定义了抽象上锁方法lock(),还有非公平尝试上锁nonfairTryAcquire(int acquires),
  1. 尝试释放锁tryRelease(int releases) 、是否持有互斥锁isHeldExclusively()等方法。
  1. Sync 有两个子类,非公平锁NonfairSync和公平锁FairSync。两个子类,都实现了抽象的方法上锁lock(),同时还有一个尝试上锁tryAcquire(int acquires)。在Sync的子类实现中,这个
  1. tryAcquire(int acquires)的形参acquires都是1,表示加锁1次。这个加锁次数,维护在AQS里面的变量state中,这个后面会讲。

  1. ReentrantLock 类内部,还有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()、获取加锁次数getHoldCount()

    获取等待的条件hasWaiters()等。其中,最重要,也是最常用的,是lock()、unlock()、tryLock()这些。
  1. ----------------------------------------------------------------------------------------------------------------

    挑主要的方法来讲。

    先介绍上锁lock()。
  1. public void lock() {
  2. sync.lock();
  3. }

在这个方法中,sync.lock(),是一个策略模式,由子类的实现而确定。如果子类是公平锁FairSync,则调用FairSync的lock()方法;否则,调用非公平锁

NonfairSync的lock()方法。

先看公平锁的lock(),代码如下

  1. final void lock() {
  2. acquire(1);
  3. }
  1. //加锁
  1. public final void acquire(int arg) {
    //如果尝试上锁上锁,并且获取队列成功,则当前线程自中断。
    if (!tryAcquire(arg) && //这里的tryAcquire,由子类实现,如下面的代码2。从这里可以看出,非公平所,acquire获取锁的时候,会直接尝试获取锁。失败则加入资源队列
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt(); // 自中断
    }

    //通过自旋的方式获取同步状态。所谓自旋,说白了,就是死循环for(;;)。这个方法返回中断状态
    //代码1
  1. final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
    boolean interrupted = false;
    for (;;) {
    final Node p = node.predecessor();
    if (p == head && tryAcquire(arg)) { //如果前驱节点是头节点并且获取锁成功,则把头节点设置成当前的节点。并且把前驱节点的next设置为null,方便gc。这里再次调用了tryAcquire
  1. setHead(node);
    p.next = null; // help GC //注意这里的写法。因为当前节点已经成为头部节点,当前节点的线程关闭后,当前节点也会被回收。那么当前节点的前驱节点的next,需要设置成null,否则gc不会回收当前节点。
    failed = false;
    return interrupted; //返回当前的节点的中断状态:false
    }
    if (shouldParkAfterFailedAcquire(p, node) && //是否应该挂起失败的线程
    parkAndCheckInterrupt())
    interrupted = true;
    }
    } finally {
    if (failed)
    cancelAcquire(node);
    }
    }

    //是否应该挂起失败的线程
  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    //如果前驱节点,已经是SIGNAL,也就是-1,那么直接返回true,表示可以挂起。因为,前驱节点释放锁后,会唤醒后续的的节点
    if (ws == Node.SIGNAL)
    /*
    * This node has already set status asking a release
    * to signal it, so it can safely park.
    */
    return true;
    if (ws > 0) { //如果前驱节点已经被注销,也就是waitStatus > 0(大于0 的只有被注销的状态),则执行下面的循环
  1. /*
    * Predecessor was cancelled. Skip over predecessors and
    * indicate retry.
    */
    do { //这里不断循环,直到前驱节点的状态<=0。当等于0的时候,表示没有状态。当小于0的时候,有-1 -2 -3三种情况。其中,-3是共享模式才有,表示节点可传播。-2则是表示节点是处于条件Condition队列。-1才表示节点处于等待队列。
    node.prev = pred = pred.prev; //其实就是常用的for循环的变种而已
  1. } 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.
    */
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //采用CAS操作,设置前驱节点的状态为-1,表示释放锁后,会唤醒后驱节点
    }
    return false;
    }
  1. /挂起并设置中断
  1. private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
    }

    //代码4
    //节点取消获取锁
  1. private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
    return;

    node.thread = null;

    // Skip cancelled predecessors
    //这里跳过前驱节点。这里的实现挺巧妙的
    Node pred = node.prev;
    while (pred.waitStatus > 0) //如果前驱节点的状态>0,也就是已经被取消了,则循环向前查找前驱节点,直到前驱节点的状态 < = 0,也就是SIGNAL -1状态或者PROPAGATE -2传播状态。传播状态只在共享模式下才有用
    node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next; //前驱节点的后驱节点

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED; //设置当前节点状态为取消,也就是1

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) { //如果当前节点是末尾节点,当前节点n会被设置成CANCEL,则把等待队列的末尾节点设置成当前节点的前驱节点,也就是第n-1个节点被设置成了末尾节点
    compareAndSetNext(pred, predNext, null); //将前驱节点的后驱节点设置为null,因为当前节点已经设置成了CANCELLED了,前驱节点正式成为末尾节点,也就不会再由后驱节点
    } else {
    // If successor needs signal, try to set pred's next-link
    // so it will get one. Otherwise wake it up to propagate.
    int ws;
    if (pred != head && //这里的说明,在下面说明1处详述
    ((ws = pred.waitStatus) == Node.SIGNAL ||
    (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
    pred.thread != null) {
    Node next = node.next; //当前节点的后驱节的成下一个节点
    if (next != null && next.waitStatus <= 0)
    compareAndSetNext(pred, predNext, next); //前驱节点的后驱节点本来是当前节点,现在将前驱节点的后驱节点,设置成当前节点的下一个节点。
    } else { //直接后激活驱节点。park是挂起unpark是激活
    unparkSuccessor(node);
    }

    node.next = node; // help GC //将节点的后驱节点设置成自身,方便gc。这里要注意的是,不同于其他变量,设置成null
    }
    }
  1. //说明1:如果当前节点不是头节点且线程不是空,有以下几种场景:1、前驱节点的状态已经是唤醒状态SIGNAL -1, 2、如果前驱节点不是SINGAL,会可能=0(没有状态) 或者=-3(共享模式的传播状态),则设置前驱节点的状态为SIGNAL

    //代码5
    //激活后驱节点(使后驱节点可用)
  1. private void unparkSuccessor(Node node) {
    /*
    * If status is negative (i.e., possibly needing signal) try
    * to clear in anticipation of signalling. It is OK if this
    * fails or if status is changed by waiting thread.
    */
    int ws = node.waitStatus;
    if (ws < 0) //如果当前节点的状态<0,即唤醒SINGAL -1状态 ,或者等待条件CONDITION -2状态 ,或者PROPAGATE -3 传播状态(共享模式),则将节点的状态设置成0(没有状态)
    compareAndSetWaitStatus(node, ws, 0);

    /*要激活的线程通常是在后驱节点上持有(这句话怎么意思?我的理解是,当前节点的后驱节点持有的线程,会被激活。也就是后驱节点的线程,会变成可用状态)。
    *如果后驱节点已经被取消或者被设置成null,则从末尾节点开始往前搜索,直接找到一个不是null又不是取消的节点。
    * Thread to unpark is held in successor, which is normally
    * just the next node. But if cancelled or apparently null,
    * traverse backwards from tail to find the actual
    * non-cancelled successor.
    */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    s = null;
    for (Node t = tail; t != null && t != node; t = t.prev) //这里从末尾节点开始循环,直到当前节点的下一个非CANCEL&非null的节点,可以参考下图
    if (t.waitStatus <= 0)
    s = t;
    }
    if (s != null)
    LockSupport.unpark(s.thread); //使后驱节点持有的线程可用。但是只是使线程可用,不保证线程会被执行。
    }
  1.  
  1. //代码2
    protected final boolean tryAcquire(int acquires) {
    //先拿到当前线程
  2. final Thread current = Thread.currentThread();
    //获取上锁的次数
  3. int c = getState();
  4. if (c == 0) { //如果上锁次数为0,则证明锁空闲
  5. if (!hasQueuedPredecessors() && //如果没有前驱节点Node,则证明当前节点是头节点。使用CAS方法,设置上锁次数。这个的次数,保存在AQS的state里面
  6. compareAndSetState(0, acquires)) {
  7. setExclusiveOwnerThread(current); //设置锁的持有者为当前线程
  8. return true;
  9. }
  10. }
  11. else if (current == getExclusiveOwnerThread()) { //如果锁由当前线程持有,则上锁次数+acquires。这个acquires总是1
  12. int nextc = c + acquires;
  13. if (nextc < 0) //校验参数合法行
  14. throw new Error("Maximum lock count exceeded");
  15. setState(nextc); //设置加锁次数
  16. return true;
  17. }
  18. return false;
  19. }

公平锁,总是先选择第一个节点加锁。如果锁已经被当前线程持有,当前线程再次获取锁的时候,会成功,加锁次数+1。这里体现的,就是ReenTrantLock的可重入性。

下面介绍释放锁的方法。事实上,公平锁和非公平锁的释放,都是调用了父类Sync的方法

  1. public void unlock() {
  2. sync.release(1); //这里调用的是父类AQS的释放锁的方法
  3. }

  4. //释放锁
  5. public final boolean release(int arg) {
  6. if (tryRelease(arg)) {
  7. Node h = head; //因为是公平锁,永远是头节点获取到锁,也就永远从头节点开始释放锁
  8. if (h != null && h.waitStatus != 0)
  9. unparkSuccessor(h); //代码5的激活后驱节点线程
  10. return true;
  11. }
  12. return false;
  13. }

尝试释放锁,调用的是父类Sync的方法,如下:

  1. protected final boolean tryRelease(int releases) {
  2. int c = getState() - releases; //加锁次数叠减。这里的releases总是1
  3. if (Thread.currentThread() != getExclusiveOwnerThread()) //如果释放锁的线程不是排他锁的持有线程,则抛出异常
  4. throw new IllegalMonitorStateException();
  5. boolean free = false;
  6. if (c == 0) {
  7. free = true;
  8. setExclusiveOwnerThread(null); //如果加锁次数已经是0,则设置锁的持有现场为null
  9. }
  10. setState(c); //设置加锁次数
  11. return free;
  12. }

下面介绍非公平锁

  1. final void lock() {
  2. if (compareAndSetState(0, 1)) //先采用 CAS操作尝试获取锁,成功则把当前线程设置成排他(互斥)锁线程
  3. setExclusiveOwnerThread(Thread.currentThread());
  4. else
  5. acquire(1); //否则,执行加锁操作。这里的加锁操作acquire(1),和公平锁的代码一模一样。唯一的区别,是加锁时候调用的tryAcquire,各自实现而已。
  6. }
  7.  
  8. protected final boolean tryAcquire(int acquires) {
  9. return nonfairTryAcquire(acquires); //这里的nonfairTryAcquire ,直接是调用父类Sync的方法。
  1. }
  1. //非公平锁尝试获取锁。由此可见,ReentrantLock的默认锁,是非公平锁。

    final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread(); //获取当前线程
  3. int c = getState(); //确认加锁次数。加锁次数维护在超类AQS的state里。这个state 是由volatile里(注意这个volatile内存言语的作用,是用于共享变量在多线程即时可见。
    //也就是一个线程改变了state,另一个线程马上能够看见。这个内存言语,是实现并发的基础之一)
  4. if (c == 0) { //加锁次数为0,证明锁还没有被获取
  5. if (compareAndSetState(0, acquires)) { //CAS操作,加锁。这里的acquires在ReentrantLock里,总是1
  6. setExclusiveOwnerThread(current); //设置当前线程持有排他锁
  7. return true;
  8. }
  9. }
  10. else if (current == getExclusiveOwnerThread()) { //如果加锁次数大于1,且是当前线程持有锁,则加锁次数累加
  11. int nextc = c + acquires;
  12. if (nextc < 0) // overflow
  13. throw new Error("Maximum lock count exceeded");
  14. setState(nextc); //设置加锁次数
  15. return true;
  16. }
  17. return false;
  18. }

由上面的公平锁和非公平锁的实现可以看到,实现大同小异,都是调用超类AQS的 acquire(int arg) 方法(acquire(int arg)是一个模板方法模式代码)。

不同的是,公平锁总是第一个节点才能获取到锁,这里并不符合计算机的资源最大使用思想。而非公平锁,则是由jvm调度。因此ReentrantLock默认使用的是非公平锁。

公平锁和非公平锁,都有各自的tryAcquire方法

ReentrantLock实现的基础,是AQS的虚拟双向队列CLH,具体表现在代码里,则是Node节点。AQS的队列有两种,一种是资源队列(用于唤醒等操作),一种条件队列(用于条件达到Condition)

Node节点,在AQS里面,是由volatile这个关键字,volatile同时又是内存言语。volatile的修饰,可以使功节点对于不同的线程即时可见。这是关键字,是并发的基础之一。

当一个线程想获取锁,被阻塞的时候,表现在代码里面,就是一个死循环for(;;),直到当前线程所在的节点获取到锁。

这里是类似于监听事件的原理:利用Node节点的修饰符volatile的特性。当另一个节点a(线程)释放了锁的时候,另一个线程b马上可以检测到。如果是节点b是节点a的后驱节点,则节点b可以获取到锁,而b的后驱节点c

则需要等待b释放锁后,再通知后驱节点c。这样c节点的线程,就实际形成了阻塞。

 ---------------------------------------------------------------

个人水平有限,请各位大佬指点。 

  1.  

ReentrantLock 源代码之我见的更多相关文章

  1. 信号量,semaphore源代码之我见

    信号量,Semaphore,一个限定访问线程数量的工具类,属于并发包java.util.concurrent 里面的类. Semaphore,内部提供了构造方法(包含默认的非公平信号量构造方法,已经可 ...

  2. 通过ReentrantLock源代码分析AbstractQueuedSynchronizer独占模式

    1. 重入锁的概念与作用       reentrant 锁意味着什么呢?简单来说,它有一个与获取锁相关的计数器,如果已占有锁的某个线程再次获取锁,那么lock方法中将计数器就加1后就会立刻返回.当释 ...

  3. jdk代理和cglib代理源代码之我见

    以前值是读过一遍jdk和cglib的代理,时间长了,都忘记入口在哪里了,值是记得其中的一些重点了,今天写一篇博客,当作是笔记.和以前一样,关键代码,我会用红色标记出来. 首先,先列出我的jdk代理对象 ...

  4. Spring Mvc 源代码之我见 二

    上一篇简单介绍了spring mvc 的一些基本内容 和DispatcherServlet 的doc.这一篇将会继续写我对Spring Mvc 源代码的理解.直接上代码: /** * This imp ...

  5. Spring Mvc 源代码之我见 一

    spring mvc 是一个web框架,包括controller.model.view 三大块.其中,核心在于model这个模块,用于处理请求的request. 和之前的博客一样,关键的代码,我会标注 ...

  6. Linux内核源代码分析方法

    Linux内核源代码分析方法   一.内核源代码之我见 Linux内核代码的庞大令不少人"望而生畏",也正由于如此,使得人们对Linux的了解仅处于泛泛的层次.假设想透析Linux ...

  7. 【Java并发编程实战】—–“J.U.C”:ReentrantLock之二lock方法分析

    前一篇博客简介了ReentrantLock的定义和与synchronized的差别,以下尾随LZ的笔记来扒扒ReentrantLock的lock方法.我们知道ReentrantLock有公平锁.非公平 ...

  8. Java并发包中Lock的实现原理

    1. Lock 的简介及使用 Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制.本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\l ...

  9. Android控件之GridView

    GridView是一项显示二维的viewgroup,可滚动的网格.一般用来显示多张图片. 以下模拟九宫图的实现,当鼠标点击图片时会进行相应的跳转链接. 目录结构 main.xml布局文件,存放Grid ...

随机推荐

  1. linux安装ngixn

    卸载ngxin(第一次安装请略过) 1.检查nginx是否启动,是否安装ngxin 检查是否安装nginx命令 rpm -qa|grep nginx 检查nginx是否启动命令: pa -ef|gre ...

  2. git子模块的使用

    1. 在项目中添加子模块 命令: git submodule add <url> 例子: git submodule add https://github.com/chaconinc/Db ...

  3. 转载 CoreCLR源码探索(七) JIT的工作原理(入门篇)

    转载自:https://www.cnblogs.com/zkweb/p/7687737.html 很多C#的初学者都会有这么一个疑问, .Net程序代码是如何被机器加载执行的? 最简单的解答是, C# ...

  4. Debian 11 配置优化指南

    原文地址: Debian 11 配置优化指南 - WindSpiritIT 0x00 简介 本文仅适用于配置 Debian 11 Bullseye 文中同时包含 Gnome 桌面和 KDE 桌面配置, ...

  5. vmware扩容centos根目录

    在vmware中编辑,给磁盘扩容 在centos中使用命令fdisk /dev/sda 输入n创建新分区 输入p创建主分区 回车,默认分区号 回车,默认起始扇区 回车,默认last扇区 输入t,改变分 ...

  6. 60天shell脚本计划-11/12-渐入佳境

    --作者:飞翔的小胖猪 --创建时间:2021年3月18日 --修改时间:2021年3月22日 说明 每日上传更新一个shell脚本,周期为60天.如有需求的读者可根据自己实际情况选用合适的脚本,也可 ...

  7. C语言中如何输出汉字;如何用C语言汉字编码输出汉字(超全版)

    目录 前景提要 方式一: 方式二: 1. 数组方式打印 2. 指针方式打印 3. 优化为while方式 方式三: 1. 使用结构体内数组方式 2. 使用结构体内数组指针方式 (1) 基础写法 (2) ...

  8. Qt:QNetworkAccessManager

    0.说明 QNetworkAccessManager允许应用发送Request并接受回应. 网络访问API是围绕一个QNetworkAccessManager对象构建的,该对象保留了所有它发送的请求的 ...

  9. docker-docke安装和镜像仓库安装和管理

    1.安装docker # yum install -y yum-utils device-mapper-persistent-data lvm2 # yum-config-manager \ --ad ...

  10. A Unified Deep Model of Learning from both Data and Queries for Cardinality Estimation 论文解读(SIGMOD 2021)

    A Unified Deep Model of Learning from both Data and Queries for Cardinality Estimation 论文解读(SIGMOD 2 ...