ReentrantLock原理源码详解

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ReentrantLock原理源码详解相关的知识,希望对你有一定的参考价值。

I)、AQS介绍

要理解ReentrantLock,首先必须要理清楚AQS以及AQS是什么,有哪些重要属性,以及整个ReentrantLock的继承关系结构是什么样的。这部分对于第一次接触AQS来说却是有点绕。

1、什么是AQS

个人简单理解AQS,AQS(AbstractQueuedSynchronize)按照他的字面意思:

1、Abstract抽象的,说明该类是一个抽象类,具体一些如何控制同步策略需要子类去实现。

2、Queued队列,说明该同步器需要用一个队列来存放竞争锁失败的线程,以便于锁被释放的时候唤醒他们。

3、Synchronize同步的,说明该类是用来控制共享资源同步访问的。

2、AQS中最重要的几个属性

1、state:表示锁是否被占有(0表示当前没有任何线程占用锁,1表示该锁被某个线程占用)

2、exclusiveOwnerThread:表示当前持有锁的线程,如果没有任何线程占用锁,则该属性值为空。

3、head:表示未获得锁的线程等待队列的头节点

4、tail:表示未获得锁的线程等待队列的尾节点

在这里插入图片描述

3、AQS(AbstractQueuedSynchronizer)结构

通过类结构来展示AQS的主要属性:state、head、tail、exclusiveOwnerThread(在父类中定义)

在这里插入图片描述

AQS的exclusiveOwnerThread属性在他的父类中

在这里插入图片描述

II)、ReentrantLock

一、reentrantlock类图结构

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yIAsrPkc-1623580745029)(../多线程之ReentrantLock/picture/reentrantlock结构.png)]

可以看到在reentrantlock中组合了一个AQS属性(即sync),通过reentrantlock加锁解锁等相关操作其实都是通过调用AQS同步器相关方法来实现的。所以最主要还是要掌握AQS。

二、分析流程说明

为了清楚的演示竞争加锁、解锁的流程,本文按照如下顺序:

  1. 以非公平锁代码为例
  2. 假设首先有thread-0线程最先来获取锁,并且没有竞争,thread-0线程成功获取到锁。
  3. 然后thread-1线程又来获取锁,此时由于thread-0还并没有释放锁,所以此时thread-1需要被挂起等待。
  4. 然后thread-0释放锁,唤醒thread-1线程,然后thread-1线程开始竞争锁。

按照上面这三个步骤来一一分析加锁解锁流程和源码!!!

三、ReentrantLock加锁流程及源码

1、构造方法

通过构造方法可以看出,平时我们使用ReentrantLock lock = new ReentrantLock();的方式来创建锁的时候,默认创建的是一把非公平锁。

// ReentrantLock构造方法可以看到ReentrantLock默认的同步器使用的是非公平同步器(即ReentrantLock默认是非公平锁)
public ReentrantLock() {
    sync = new NonfairSync();
}

2、thread-0线程加锁

假设这里thread-0线程调用lock.lock()方法来获取锁,并且此时锁没有被任何线程占有,则thread-0线程会成功获取到锁。通过的cas的方式将AQS的state由0设置为1并且设置ExclusiveOwnerThread为当前线程(以后可以通过ExclusiveOwnerThread直接获取到持有锁的线程)。

public void lock() {
  	// 调用同步器的lock()方法加锁
    sync.lock();
}
static final class NonfairSync extends Sync {
   
    final void lock() {
        // 通过cas的方式将state从 0 设置为 1 (cas是原子性操作,并发时只有一个线程会执行成功)
        if (compareAndSetState(0, 1))
            // 如果state设置成功,则设置该锁被当前线程独占(将当前线程标记为持有锁的线程)
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 获取锁失败的后续操作
            acquire(1);
    }
}

Thread-0线程加锁成功后非公平同步器状态

在这里插入图片描述

3、thread-1也来竞争锁

此时线程thread-1也来竞争锁了,Thread-1 执行了如下步骤

  1. Thread-1通过cas的方式尝试将 state 由 0 改为 1,结果失败

在这里插入图片描述

  1. 进入 tryAcquire 逻辑再次尝试获取锁,这时 state 已经是1,结果仍然失败

  2. 接下来进入 addWaiter 逻辑,构造 Node 队列,并将自己添加到等待队列中去

    1. 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
    2. Node 的创建是懒惰的(即有竞争的时候才会创建)
    3. 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lv3icdgz-1623580745036)(../多线程之ReentrantLock/picture/thread-1竞争锁添加node.png)]

4、thread-1竞争锁流程源码

①、调用lock()方法获取锁
static final class NonfairSync extends Sync {
   	// thread-1同样调用了lock方法来获取锁,此时cas获取锁失败,进入acquire方法
    final void lock() {
        // 步骤一、通过cas的方式将state从 0 设置为 1 (cas是原子性操作,并发时只有一个线程会执行成功)
        if (compareAndSetState(0, 1))
            // 如果state设置成功,则设置该锁被当前线程独占(将当前线程标记为持有锁的线程)
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 步骤二、获取锁失败的后续操作
            acquire(1);
    }
}

public final void acquire(int arg) {
    // 再次尝试获取锁,如果还是没有获取到(即 tryAcquire(arg) 返回为false),则将该线程加入等待队列
    // tryAcquire(arg) : 再次尝试获取锁,获取成功返回true,失败返回false
    // addWaiter(Node.EXCLUSIVE) : 添加到阻塞链表中
    // acquireQueued : 将该节点挂起
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
②、tryAcquire方法

tryAcquire分为两大步骤:

①、首先判断AQS的state是否等于0(即未加锁),如果是则通过cas的方式将该值改为1,并且将ExclusiveOwnerThread设置为当前线程,表示获得了锁。

②、如果state不等于0,则通过exclusiveOwnerThread去判断当前持有锁的线程是否是当前线程(这里就体现了exclusiveOwnerThread的用处了),如果是则将 state的值加一,表示重入锁。

③、如果以上两种情况情况都不满足,则表示尝试加锁失败。返回false。

/**
 * 非公平锁的尝试获取锁
 */
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
  // 入参 acquires = 1
  // 获得当前正在运行的线程
  final Thread current = Thread.currentThread();
  // 获取state变量的值,即当前当前锁被重入的次数
  int c = getState();
  // ①、如果c=0,则证明当前锁没有被任何线程持有
  if (c == 0) {
    // 尝试自己通过CAS获取锁
    if (compareAndSetState(0, acquires)) {
      // 如果获取锁成功,则将当前线程设置为持有线程
      setExclusiveOwnerThread(current);
      // 返回获取锁成功
      return true;
    }
  }
  // ②、如果c != 0 并且 exclusiveOwnerThread = 当前线程,说明当前持有锁的是自己(该处对应重入锁)
  // 如果当前线程就是锁的持有线程
  else if (current == getExclusiveOwnerThread()) {
    // 将state加一(即将重入次数加一)
    int nextc = c + acquires;
    if (nextc < 0) // overflow
      throw new Error("Maximum lock count exceeded");
    // 设置state新值
    setState(nextc);
    // 返回锁的获取结果
    return true;
  }
  // ③、否则返回获取锁失败
  return false;
}
③、addWaiter方法
/**
 * @return the new node
 * 添加节点到等待链表尾部
 */
private Node addWaiter(Node mode) {
    // 为当前线程创建一个节点,并将线程实例封装在该节点中,mode这里为null
    Node node = new Node(Thread.currentThread(), mode);
    // pred 指向 尾节点 tail
    Node pred = tail;
    // 如果尾节点不等于空
    // 尝试快速入队
    if (pred != null) {
        // 将当前节点的prev指向尾节点,这里有可能多个node的prev指针都指向了这个最后的节点
        // 如何解决呢?其实就是通过下面的 compareAndSetTail(pred, node) 原子操作,将AQS的tail指针指向这后面的多个节点中的一个
        // 这里用cas保证只有一个节点能被指向.其他没有被指向的线程节点就会去执行下面的自旋入队逻辑 enq(node);
        node.prev = pred;
        // 通过cas的方式将尾节点的下一个节点next指向当前节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            // 返回当前节点
            return node;
        }
    }
    // 自旋入队,添加节点到双向链表尾部
    enq(node);
    // 返回当前节点
    return node;
}
④、acquireQueued
  1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞

  2. 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败

  3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1(-1表示该节点释放锁后需要唤醒他的下一个节点,当然这时head的waitstate = 0),所以这次循环返回 false,进行下一次循环。

    在这里插入图片描述

  4. acquireQueued进行下一次循环 ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败

  5. 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true

  6. 进入 parkAndCheckInterrupt将当前线程阻塞(实际上调用的就是LockSupport.park(this);), Thread-1 park(灰色表示)

在这里插入图片描述

再次有多个线程经历上述过程竞争失败,变成这个样子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bHUmeT9N-1623580745038)(../多线程之ReentrantLock/picture/多个线程竞争锁失败.png)]

源代码:

/**
 * 挂起节点
 */
final boolean acquireQueued(final Node node, int arg) {
    // 失败标识
    boolean failed = true;
    try {
        // 中断标识
        boolean interrupted = false;
        for (;;) {
            // 获取当前新节点的前一个节点
            final Node p = node.predecessor();
            // 如果该节点的前一个节点是头节点,则说明很快就要轮到自己获取锁了,
            // 所以在这里会再次调用 tryAcquire 再次去尝试一次获取锁
            if (p == head && tryAcquire(arg)) {
                // 如果获取到了锁,则将自己设置为头节点
                setHead(node);
                // 将p的next置为空,方便GC回收
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 判断该节点获取锁失败后(即上个if条件不满足时),是否应该被挂起到阻塞链表中(shouldParkAfterFailedAcquire)
            // 如果 shouldParkAfterFailedAcquire 返回为true,则该节点被添加到阻塞链表中
            // parkAndCheckInterrupt : 挂起节点
            // 没有获取锁的节点需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果失败
        if (failed)
            // 取消获得锁,资源回收
            cancelAcquire(node);
    }
}

/**
 * 判断一个节点在获取锁失败后,是否应该被挂起
 * 注意:是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定,
 * 而不是本节点的waitStatus 决定
 * 如果该节点的前一个节点的waitset为-1时该节点才能被挂起。其他情况都不能被挂起,为什么呢?
 * 因为节点的waitset值 = -1 ,表示该节点释放锁后需要唤醒他的下一个节点,如果前一个节点的waitset值不等于-1
 * 那么此时如果将这个节点挂起,那么该节点以后永远也不会被唤醒了。所以不能挂起
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  // 该节点的前一个节点的等待状态
  int ws = pred.waitStatus;
  // 如果前一个节点是正常等待唤醒状态,则返回true.说明该节点应该被挂起
  if (ws == Node.SIGNAL)
    return true;
  
  // 前一个节点被取消或等待超时,此时需要循环的往前寻找,找到一个正常的节点,然后把当前节点加到正常节点后面
  if (ws > 0) {
    // 循环查找正常的节点,并且将该节点加入到正常节点后面
    do {
      node.prev = pred = pred.prev;
    } while (pred.waitStatus > 0);
    pred.next = node;
  } else {
    // 否则,将前一个节点的waitStatus设置为SIGNAL(SIGNAL=-1,代表正常状态,并且当该节点释放锁以后需要唤醒他的下一个节点)
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  }
  return false;
}

四、ReentrantLock释放锁流程及源码

①、释放锁流程

假设此时thread-0执行完任务了,需要释放锁,线程thread-0释放锁有两个步骤:

  • 设置 exclusiveOwnerThread 为 null
  • 设置state = 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5nFCFLEF-1623580745039)(../多线程之ReentrantLock/picture/线程thread-0释放锁.png)]

当前等待队列不为 null,并且 head 的 waitStatus = -1说明需要唤醒下一个节点,所以进入 unparkSuccessor 流程。找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 。Thread-1 被唤醒后继续竞争锁,进入 acquireQueued 流程。

情况一、如果此时 thread-1 竞争锁成功,则会设置:

  • exclusiveOwnerThread 为 Thread-1,state = 1
  • head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
  • 原本的 head 因为从链表断开,而可被垃圾回收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AiFuTmZZ-1623580745039)(../多线程之ReentrantLock/picture/thread-0释放锁thread-1竞争锁.png)]

情况二、如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了,如果不巧刚好锁又被 Thread-4 占了先,则:

  • Thread-4 被设置为 exclusiveOwnerThread,state = 1
  • Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2VidDCAT-1623580745040)(../多线程之ReentrantLock/picture/thread-1和thread-4同时竞争锁.png)]

②、释放锁源码

public void unlock() {
    sync.release(1);
}

/**
 * 释放锁
 */
public final boolean release(int arg) {
  // 先尝试释放锁
  if (tryRelease(arg)) {
    Node h = head;
    // 当前等待队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
    if (h != null && h.waitStatus != 0)
      // 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行
      unparkSuccessor(h);
    return true;
  }
  return false;
}

/**
 * 尝试释放锁
 */
protected final boolean tryRelease(int releases) {
  // 计算更新的state值
  int c = getState() - releases;
  // 释放锁的时候如果判断到当前持有锁的线程不是自己,则抛出异常(自己只能释放自己加的锁)
  if (Thread.currentThread() != getExclusiveOwnerThread())
    throw new IllegalMonitorStateException();
  boolean free = false;
  // 如果c==0,则说明需要释放锁,直接将free置为true,并且设置当前独占线程为空
  // 支持锁重入, 只有 state 减为 0, 才释放成功
  if (c == 0) {
    free = true;
    setExclusiveOwnerThread(null);
  }
  // 更新state状态(设置重入次数标志)
  setState(c);
  return free;
}
/**
 * 找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1 
 */
private void unparkSuccessor(Node node) {
  
  int ws = node.waitStatus;
  if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);

  Node s = node.next;
  if (s == null || s.waitStatus > 0) {
    s = null;
    // 找到队列中离 head 最近的一个 Node(没取消的)
    for (Node t = tail; t != null && t != node; t = t.prev)
      if (t.waitStatus <= 0)
        s = t;
  }
  if (s != null)
    // unpark 恢复其运行
    LockSupport.unpark(s.thread);
}

以上是关于ReentrantLock原理源码详解的主要内容,如果未能解决你的问题,请参考以下文章

源码分析ReentrantLock实现原理

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

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

JDK并发源码分析:ReentrantLock原理

ReentrantLock实现原理及源码分析

并发编程—— ReentrantLock实现原理及源码分析