AQS实现原理分析——ReentrantLock

Stella981
• 阅读 657

在Java并发包java.util.concurrent中可以看到,不少源码是基于AbstractQueuedSynchronizer(以下简写AQS)这个抽象类,因为它是Java并发包的基础工具类,是实现ReentrantLock、CountDownLatch、Semaphore、FutureTask 等类的基础。 
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在抽象方法中免不了要对同步状态进行更改,这时就需要使用AQS提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update) 来进行操作,因为他们能够保证状态的改变是安全的 。

1 AQS原理介绍

在 AQS 内部,通过维护一个FIFO队列来管理多线程的排队工作。在公平竞争的情况下,无法获取同步状态的线程将会被封装成一个节点,置于队列尾部。入队的线程将会通过自旋的方式获取同步状态,若在有限次的尝试后,仍未获取成功,线程则会被阻塞住。大致示意图如下: 
AQS实现原理分析——ReentrantLock  
当头结点释放同步状态后,且后继节点对应的线程被阻塞,此时头结点线程将会去唤醒后继节点线程。后继节点线程恢复运行并获取同步状态后,会将旧的头结点从队列中移除,并将自己设为头结点。大致示意图如下: 
AQS实现原理分析——ReentrantLock

2 AQS结构

2.1 AQS属性

先来看看 AQS 有哪些变量,搞清楚这些基本就知道 AQS 是什么套路了,毕竟可以猜嘛!

  1. // 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的
  2. private transient volatile Node head;
  3. // 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个隐视的链表
  4. private transient volatile Node tail;
  5. // 这个是最重要的,不过也是最简单的,代表当前锁的状态,0代表没有被占用,大于0代表有线程持有当前锁
  6. // 之所以说大于0,而不是等于1,是因为锁可以重入嘛,每次重入都加上1
  7. private volatile int state;
  8. // 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入
  9. // reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁
  10. // if (currentThread == getExclusiveOwnerThread()) {state++}
  11. private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

在并发的情况下,AQS 会将未获取同步状态的线程将会封装成节点,并将其放入同步队列尾部。同步队列中的节点除了要保存线程,还要保存等待状态。不管是独占式还是共享式,在获取状态失败时都会用到节点类。所以这里我们要先看一下节点类的实现,为后面的源码分析进行简单铺垫。既然AQS通过一个双向链表来维护所有的的节点,那么先看一下每一个节点的结构:

  1. static final class Node {

  2. /** Marker to indicate a node is waiting in shared mode */

  3. // 标识节点当前在共享模式下

  4. static final Node SHARED = new Node();

  5. /** Marker to indicate a node is waiting in exclusive mode */

  6. // 标识节点当前在独占模式下

  7. static final Node EXCLUSIVE = null;

  8. // ======== 下面的几个int常量是给waitStatus用的 ===========

  9. /** waitStatus value to indicate thread has cancelled */

  10. // 代码此线程取消了争抢这个锁

  11. static final int CANCELLED = 1;

  12. /** waitStatus value to indicate successor's thread needs unparking */

  13. // 官方的描述是,其表示当前node的后继节点对应的线程需要被唤醒

  14. static final int SIGNAL = -1;

  15. /** waitStatus value to indicate thread is waiting on condition */

  16. // 本文不分析condition,所以略过吧,下一篇文章会介绍这个

  17. static final int CONDITION = -2;

  18. /**

  19. * waitStatus value to indicate the next acquireShared should

  20. * unconditionally propagate

  21. */

  22. // 同样的不分析,略过吧

  23. static final int PROPAGATE = -3;

  24. // =====================================================

  25. // 取值为上面的1、-1、-2、-3,或者0(以后会讲到)

  26. // 这么理解,暂时只需要知道如果这个值 大于0 代表此线程取消了等待,

  27. // 也许就是说半天抢不到锁,不抢了,ReentrantLock是可以指定timeouot的。。。

  28. volatile int waitStatus;

  29. // 前驱节点的引用

  30. volatile Node prev;

  31. // 后继节点的引用

  32. volatile Node next;

  33. // 这个就是线程本尊

  34. volatile Thread thread;

  35. }

Node 的数据结构其实也挺简单的,就是 thread + waitStatus + pre + next 四个属性而已。

2.1 AQS方法

本节将介绍三组重要的方法,通过使用这三组方法即可实现一个同步组件。

第一组方法是用于访问/设置同步状态的,如下:

方法

描述

int getState()

获取同步状态

void setState()

设置同步状态

boolean compareAndSetState(int expect, int update)

通过 CAS 设置同步状态

第二组方需要由同步组件覆写。如下:

方法

描述

boolean tryAcquire(int arg)

独占式获取同步状态

boolean tryRelease(int arg)

独占式释放同步状态

int tryAcquireShared(int arg)

共享式获取同步状态

boolean tryReleaseShared(int arg)

共享式私房同步状态

boolean isHeldExclusively()

检测当前线程是否获取独占锁

第三组方法是一组模板方法,同步组件可直接调用。如下:

方法

描述

void acquire(int arg)

独占式获取同步状态,该方法将会调用 tryAcquire 尝试获取同步状态。获取成功则返回,获取失败,线程进入同步队列等待。

void acquireInterruptibly(int arg)

响应中断版的 acquire

boolean tryAcquireNanos(int arg,long nanos)

超时+响应中断版的 acquire

void acquireShared(int arg)

共享式获取同步状态,同一时刻可能会有多个线程获得同步状态。比如读写锁的读锁就是就是调用这个方法获取同步状态的。

void acquireSharedInterruptibly(int arg)

响应中断版的 acquireShared

boolean tryAcquireSharedNanos(int arg,long nanos)

超时+响应中断版的 acquireShared

boolean release(int arg)

独占式释放同步状态

boolean releaseShared(int arg)

共享式释放同步状态

3 ReentrantLock加锁

ReentrantLock 在内部用了内部类 Sync 来管理锁,所以真正的获取锁和释放锁是由 Sync 的实现类来控制的。 
Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁),我们看 NonfairSync 部分。

3.1 NonfairSync锁

加锁的一步如下:

  1. public void lock() {
  2. sync.lock();
  3. }

对于非公平锁,则执行如下流程:

  1. final void lock() {
  2. // 如果锁没有被任何线程锁定且加锁成功则设定当前线程为锁的拥有者
  3. if (compareAndSetState(0, 1))
  4. setExclusiveOwnerThread(Thread.currentThread());
  5. else
  6. acquire(1);
  7. }

1)我们假设在这个时候,还没有任务线程获取锁,这个时候,第一个线程过来了(我们使用的是非公平锁),那么第一个线程thread1会去获取锁,这时它会调用下面的方法,通过CAS的操作,将当前AQS的state由0变成1,证明当前thread1已经获取到锁,并且将AQS的exclusiveOwnerThread设置成thread1,证明当前持有锁的线程是thread1。 
2)如果此时来了第二个线程thread2,并且我们假设thread1还没有释放锁,因为我们使用的是非公平锁,那么thread2首先会进行抢占式的去获取锁,调用NonFairSync.lock方法获取锁。NonFairSync.lock方法的第一个分支是通过CAS操作获取锁,很明显,这一步肯定会失败,因为此时thread1还没有释放锁。那么thread2将会走NonFairSync.lock方法的第二个分支,进行acquire(1)操作。acquire(1)其实是AQS的方法,acquire(1)方法内部首先调用tryAcquire方法,ReentrantLock.NonFairLock重写了tryAcquire方法,并且ReentrantLock.NonFairLock的tryAcquire方法又调用了ReentrantLock.Sync的nonfairTryAcquire方法,nonfairTryAcquire方法如下:

  1. // 该方法来自父类AQS,我直接贴过来这边,下面分析的时候同样会这样做,不会给读者带来阅读压力

  2. // 我们看到,这个方法,如果tryAcquire(arg) 返回true, 也就结束了。

  3. // 否则,acquireQueued方法会将线程压到队列中

  4. public final void acquire(int arg) {

  5. // 首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试

  6. // 因为有可能直接就成功了呢,也就不需要进队列排队了,

  7. // 对于公平锁的语义就是:本来就没人持有锁,根本没必要进队列等待(又是挂起,又是等待被唤醒的)

  8. if (!tryAcquire(arg) &&

  9. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

  10. selfInterrupt();

  11. }

  12. protected final boolean tryAcquire(int acquires) {

  13. //直接调用下面方法

  14. return nonfairTryAcquire(acquires);

  15. }

  16. /**

  17. * Performs non-fair tryLock. tryAcquire is implemented in

  18. * subclasses, but both need nonfair try for trylock method.

  19. */

  20. // 尝试直接获取锁,返回值是boolean,代表是否获取到锁

  21. // 返回true:1.没有线程在等待锁;2.重入锁,线程本来就持有锁,也就可以理所当然可以直接获取

  22. final boolean nonfairTryAcquire(int acquires) {

  23. final Thread current = Thread.currentThread();

  24. int c = getState();

  25. // state == 0 此时此刻没有线程持有锁

  26. if (c == 0) {

  27. // 用CAS尝试一下,成功了就获取到锁了,

  28. // 不成功的话,只能说明一个问题,就在刚刚几乎同一时刻有个线程抢先了 =_=

  29. // 因为刚刚还没人的,我判断过了???

  30. if (compareAndSetState(0, acquires)) {

  31. // 到这里就是获取到锁了,标记一下,告诉大家,现在是我占用了锁

  32. setExclusiveOwnerThread(current);

  33. return true;

  34. }

  35. }

  36. else if (current == getExclusiveOwnerThread()) {

  37. // 会进入这个else if分支,说明是重入了,需要操作:state=state+1

  38. int nextc = c + acquires;

  39. if (nextc < 0) // overflow

  40. throw new Error("Maximum lock count exceeded");

  41. setState(nextc);

  42. return true;

  43. }

  44. // 如果到这里,说明前面的if和else if都没有返回true,说明没有获取到锁

  45. // 回到上面一个外层调用方法继续看:

  46. // if (!tryAcquire(arg)

  47. // && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

  48. // selfInterrupt();

  49. return false;

  50. }

nonfairTryAcquire方法的执行逻辑如下: 
1. 获取当前将要去获取锁的线程,在此时的情况下,也就是我们的thread2线程。 
2. 获取当前AQS的state的值。如果此时state的值是0,那么我们就通过CAS操作获取锁,然后设置AQS的exclusiveOwnerThread为thread2。很明显,在当前的这个执行情况下,state的值是1不是0,因为我们的thread1还没有释放锁。 
3. 如果当前将要去获取锁的线程等于此时AQS的exclusiveOwnerThread的线程,则此时将state的值加1,很明显这是重入锁的实现方式。在此时的运行状态下,将要去获取锁的线程不是thread1,也就是说这一步不成立。 
4. 以上操作都不成立的话,我们直接返回false。 
既然返回了false,那么之后就会调用addWaiter方法,这个方法负责把当前无法获取锁的线程包装为一个Node添加到队尾。通过下面的代码片段我们就知道调用逻辑:

  1. // 假设tryAcquire(arg) 返回false,那么代码将执行:
  2. // acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
  3. // 这个方法,首先需要执行:addWaiter(Node.EXCLUSIVE)
  4. /**
  5. * Creates and enqueues node for current thread and given mode.
  6. *
  7. * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
  8. * @return the new node
  9. */
  10. // 此方法的作用是把线程包装成node,同时进入到队列中
  11. // 参数mode此时是Node.EXCLUSIVE,代表独占模式
  12. private Node addWaiter(Node mode) {
  13. Node node = new Node(Thread.currentThread(), mode);
  14. // Try the fast path of enq; backup to full enq on failure
  15. //以下几行代码想把当前node加到链表的最后面去,也就是进到阻塞队列的最后
  16. Node pred = tail;
  17. // tail!=null => 队列不为空(tail==head的时候,其实队列是空的,不过不管这个吧)
  18. if (pred != null) {
  19. // 设置自己的前驱 为当前的队尾节点
  20. node.prev = pred;
  21. // 用CAS把自己设置为队尾, 如果成功后,tail == node了
  22. if (compareAndSetTail(pred, node)) {
  23. // 进到这里说明设置成功,当前node==tail, 将自己与之前的队尾相连,
  24. // 上面已经有 node.prev = pred
  25. // 加上下面这句,也就实现了和之前的尾节点双向连接了
  26. pred.next = node;
  27. // 线程入队了,可以返回了
  28. return node;
  29. }
  30. // 仔细看看上面的代码,如果会到这里,
  31. // 说明 pred==null(队列是空的) 或者 CAS失败(有线程在竞争入队)
  32. // 读者一定要跟上思路,如果没有跟上,建议先不要往下读了,往回仔细看,否则会浪费时间的
  33. enq(node);
  34. return node;
  35. }

很明显在addWaiter内部: 
第一步:将当前将要去获取锁的线程也就是thread2和独占模式封装为一个node对象。并且我们也知道在当前的执行环境下,线程阻塞队列是空的,因为thread1获取了锁,thread2也是刚刚来请求锁,所以线程阻塞队列里面是空的。很明显,这个时候队列的尾部tail节点也是null,那么将直接进入到enq方法。 
第二步:我们首先看下enq方法的内部实现。首先内部是一个自旋循环。

  1. /**

  2. * Inserts node into queue, initializing if necessary. See picture above.

  3. * @param node the node to insert

  4. * @return node's predecessor

  5. */

  6. // 采用自旋的方式入队

  7. // 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队,

  8. // 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的

  9. private Node enq(final Node node) {

  10. for (;;) {

  11. Node t = tail;

  12. // 之前说过,队列为空也会进来这里

  13. if (t == null) { // Must initialize

  14. // 初始化head节点

  15. // 细心的读者会知道原来head和tail初始化的时候都是null,反正我不细心

  16. // 还是一步CAS,你懂的,现在可能是很多线程同时进来呢

  17. if (compareAndSetHead(new Node()))

  18. // 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了

  19. // 这个时候有了head,但是tail还是null,设置一下,

  20. // 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了

  21. // 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return

  22. // 所以,设置完了以后,继续for循环,下次就到下面的else分支了

  23. tail = head;

  24. } else {

  25. // 下面几行,和上一个方法 addWaiter 是一样的,

  26. // 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排

  27. node.prev = t;

  28. if (compareAndSetTail(t, node)) {

  29. t.next = node;

  30. return t;

  31. }

  32. }

  33. }

  34. }

第一次循环:t为null,随后我们new出了一个空的node节点,并且通过CAS操作设置了线程的阻塞队列的head节点就是我们刚才new出来的那个空的node节点,其实这是一个“假节点”,那么什么是“假节点”呢?那就是节点中不包含线程。设置完head节点后,同时又将head节点赋值给尾部tail节点,到此第一次循环结束。此时的节点就是如下: 
AQS实现原理分析——ReentrantLock  
第二次循环:现在判断维度tail已经不是null了,那么就走第二个分支了,将尾部tail节点赋值给我们传递进来的节点Node的前驱节点,此时的结构如下: 
AQS实现原理分析——ReentrantLock  
然后再通过CAS的操作,将我们传递进来的节点node设置成尾部tail节点,并且将我们的node节点赋值给原来的老的尾部节点的后继节点,此时的结构如下: 
AQS实现原理分析——ReentrantLock  
这个时候代码中使用了return关键字,也就是证明我们经过了2次循环跳出了这个自悬循环体系。

在执行addWaiter将节点加入阻塞队列后,接下来将会调用acquireQueued方法,主要是判断当前节点的前驱节点是不是head节点,如果是的话,就再去尝试获取锁,如果不是,就挂起当前线程。这里可能有人疑问了,为什么判断当前节点的前驱节点是head节点的话就去尝试获取锁呢?因为我们知道head节点是一个假节点,如果当前的节点的前驱节点是头节点即是假节点的话,那么这个假节点的后继节点就有可能有获取锁的机会,所以我们需要去尝试。 
现在我们看下acquireQueued方法内部,我们也可以清楚的看到,这个方法的内部也是一个自悬循环。:

  1. // 现在,又回到这段代码了

  2. // if (!tryAcquire(arg)

  3. // && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

  4. // selfInterrupt();

  5. // 下面这个方法,参数node,经过addWaiter(Node.EXCLUSIVE),此时已经进入阻塞队列

  6. // 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话,

  7. // 意味着上面这段代码将进入selfInterrupt(),所以正常情况下,下面应该返回false

  8. // 这个方法非常重要,应该说真正的线程挂起,然后被唤醒后去获取锁,都在这个方法里了

  9. /**

  10. * Acquires in exclusive uninterruptible mode for thread already in

  11. * queue. Used by condition wait methods as well as acquire.

  12. *

  13. * @param node the node

  14. * @param arg the acquire argument

  15. * @return {@code true} if interrupted while waiting

  16. */

  17. final boolean acquireQueued(final Node node, int arg) {

  18. boolean failed = true;

  19. try {

  20. boolean interrupted = false;

  21. for (;;) {

  22. final Node p = node.predecessor();

  23. // p == head 说明当前节点虽然进到了阻塞队列,但是是阻塞队列的第一个,因为它的前驱是head

  24. // 注意,阻塞队列不包含head节点,head一般指的是占有锁的线程,head后面的才称为阻塞队列

  25. // 所以当前节点可以去试抢一下锁

  26. // 这里我们说一下,为什么可以去试试:

  27. // 首先,它是队头,这个是第一个条件,其次,当前的head有可能是刚刚初始化的node,

  28. // enq(node) 方法里面有提到,head是延时初始化的,而且new Node()的时候没有设置任何线程

  29. // 也就是说,当前的head不属于任何一个线程,所以作为队头,可以去试一试,

  30. // tryAcquire已经分析过了, 忘记了请往前看一下,就是简单用CAS试操作一下state

  31. if (p == head && tryAcquire(arg)) {

  32. setHead(node);

  33. p.next = null; // help GC

  34. failed = false;

  35. return interrupted;

  36. }

  37. // 到这里,说明上面的if分支没有成功,要么当前node本来就不是队头,

  38. // 要么就是tryAcquire(arg)没有抢赢别人,继续往下看

  39. if (shouldParkAfterFailedAcquire(p, node) &&

  40. parkAndCheckInterrupt())

  41. interrupted = true;

  42. }

  43. } finally {

  44. if (failed)

  45. cancelAcquire(node);

  46. }

  47. }

第一次循环:获取我们传入node的前驱节点,判断是否是head节点,现在我们的状态是: 
AQS实现原理分析——ReentrantLock  
很明显满足当前node节点的前驱节点是head节点,那么现在我们就要去调用tryAcquire方法,也就是NonfairSync类的tryAcquire方法,而这个方法又调用了ReentrantLock.Sync.nonfairTryAcquire方法。

很明显thread1占用锁,所以thread2获取锁是失败的,直接返回false。按照调用流程,现在进入了当前节点的前驱节点的shouldParkAfterFailedAcquire方法,检查当前节点的前驱节点的waitstatus。shouldParkAfterFailedAcquire方法内部如下:

  1. /**
  2. * Checks and updates status for a node that failed to acquire.
  3. * Returns true if thread should block. This is the main signal
  4. * control in all acquire loops. Requires that pred == node.prev.
  5. *
  6. * @param pred node's predecessor holding status
  7. * @param node the node
  8. * @return {@code true} if thread should block
  9. */
  10. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  11. int ws = pred.waitStatus;
  12. // 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true
  13. if (ws == Node.SIGNAL)
  14. /*
  15. * This node has already set status asking a release
  16. * to signal it, so it can safely park.
  17. */
  18. return true;
  19. // 前驱节点 waitStatus大于0 ,之前说过,大于0 说明前驱节点取消了排队。这里需要知道这点:
  20. // 进入阻塞队列排队的线程会被挂起,而唤醒的操作是由前驱节点完成的。
  21. // 所以下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点,
  22. // 简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队,
  23. // 找前驱节点的前驱节点做爹,往前循环总能找到一个好爹的
  24. if (ws > 0) {
  25. /*
  26. * Predecessor was cancelled. Skip over predecessors and
  27. * indicate retry.
  28. */
  29. do {
  30. node.prev = pred = pred.prev;
  31. } while (pred.waitStatus > 0);
  32. pred.next = node;
  33. } else {
  34. /*
  35. * waitStatus must be 0 or PROPAGATE. Indicate that we
  36. * need a signal, but don't park yet. Caller will need to
  37. * retry to make sure it cannot acquire before parking.
  38. */
  39. // 仔细想想,如果进入到这个分支意味着什么
  40. // 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3
  41. // 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0
  42. // 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1)
  43. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  44. }
  45. return false;
  46. }

上面的流程如下: 
1 如果当前节点的前驱节点的waitStatus=-1,就直接返回true; 
2 如果当前节点的前驱节点的waitStatus>0,也就是前驱节点被取消了,那么就从阻塞队列中删除前驱节点; 
3 如果以上情况都不满足,则通过CAS操作将前驱节点设置为-1(SIGNAL)。 
此时,阻塞队列中,会将head节点的waitStatus由0变为-1(初始化节点的waitStatus都是0),然后返回false; 
AQS实现原理分析——ReentrantLock  
然后回到acquireQueued执行第二次循环:很明显满足当前node节点的前驱节点是head节点,那么现在我们就要去调用tryAcquire方法,也就是NonfairSync类的tryAcquire方法,而这个方法又调用了ReentrantLock.Sync.nonfairTryAcquire方法。很明显此时thread2获取锁是失败的,直接返回false。按照调用流程,现在进入了当前节点的前驱节点的shouldParkAfterFailedAcquire方法,检查当前节点的前驱节点的waitstatus。此时waitstatus为-1,这个方法返回true。shouldParkAfterFailedAcquire返回true后,就会调用parkAndCheckInterrupt方法,直接将当前线程thread2中断。

  1. // 1. 如果shouldParkAfterFailedAcquire(p, node)返回true,

  2. // 那么需要执行parkAndCheckInterrupt():

  3. // 这个方法很简单,因为前面返回true,所以需要挂起线程,这个方法就是负责挂起线程的

  4. // 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒=======

  5. private final boolean parkAndCheckInterrupt() {

  6. LockSupport.park(this);

  7. return Thread.interrupted();

  8. }

仔细看这个方法acquireQueued方法,是无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,在这里,当然不会出现死循环。因为parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。分析到这里,我们的thread2线程已经被中断了,这个线程不会再继续执行下去了。

3)假设现在我们的thread1还没有释放锁,而现在又来了一个线程thread3。 
thread3首先调用lock方法获取锁,首先去抢占锁,因为我们知道thread1还没有释放锁,这个时候thread3肯定抢占失败,于是又调用了acquire方法,接着又失败。接着会去调用addWaiter方法,将当前线程thread3封装成node加入到线程阻塞队列的尾部。现在的结构如下: 
AQS实现原理分析——ReentrantLock  
然后,调用addWaiter方法后,第一步,将当前将要去获取锁的线程也就是thread3和独占模式封装为一个node对象。并且我们也知道在当前的执行环境下,线程阻塞队列不是空的,因为thread2获取了锁,thread2已经加入了队列。很明显,这个时候队列的尾部tail节点也不是null,那么将直接进入到if分支。将尾部tail节点赋值给我们传入的node节点的前驱节点。如下: 
AQS实现原理分析——ReentrantLock
第二步:通过CAS将我们传递进来的node节点设置成tail节点,并且将新tail节点设置成老tail节点的后继节点。 
AQS实现原理分析——ReentrantLock
在执行addWaiter方法,将thread3插入到阻塞队列尾部后,然后继续调用acquireQueued方法,这是一个自循环方法。

  1. final boolean acquireQueued(final Node node, int arg) {
  2. boolean failed = true;
  3. try {
  4. boolean interrupted = false;
  5. for (;;) {
  6. final Node p = node.predecessor();
  7. if (p == head && tryAcquire(arg)) {
  8. setHead(node);
  9. p.next = null; // help GC
  10. failed = false;
  11. return interrupted;
  12. }
  13. if (shouldParkAfterFailedAcquire(p, node) &&
  14. parkAndCheckInterrupt())
  15. interrupted = true;
  16. }
  17. } finally {
  18. if (failed)
  19. cancelAcquire(node);
  20. }
  21. }

第一次循环:获取thread3节点的前驱节点,判断是否是head节点,现在阻塞队列的结果如下: 
AQS实现原理分析——ReentrantLock
由于thread3的节点的前驱节点是thread2,不是head,所以会直接调用shouldParkAfterFailedAcquire方法: 
1 判断thread3节点的前驱节点的waitStatus,状态为-1,直接返回true; 
2 如果thread3节点的前驱节点的waitStatus大于0,说明这个节点被CANCEL了,一直循环向前查找,直到找到waitStatus<=0; 
3 如果都不是以上的情况,就通过CAS操作将这个前驱节点设置成-1(SIGHNAL)。 
此时的结构如下,主要是thread2节点的waitStatus由0变成了-1。 
AQS实现原理分析——ReentrantLock
第二次循环:获取thread3节点的前驱节点,判断是否是head节点,由于明显不是head节点,那么直接进入调用shouldParkAfterFailedAcquire方法,此时,thread3节点的前驱节点的waitStatus为-1,因为返回ture,所以接下来会调用parkAndCheckInterrupt方法,直接将当前线程thread3线程中断。现在thread2和thread3线程都被中断了。

3.2 FairSync锁

其实,公平锁和非公平锁,不同点只存在两个地方: 
1 对于非公平锁,获取锁的第一步就是通过CAS设置state的状态,如果成功,则直接获取了锁; 
2 对于公平是和非公平锁,都会调用tryAcquire方法来获取锁,但是二者是有区别的:

  1. //非公平锁

  2. /**

  3. * Performs non-fair tryLock. tryAcquire is implemented in

  4. * subclasses, but both need nonfair try for trylock method.

  5. */

  6. final boolean nonfairTryAcquire(int acquires) {

  7. final Thread current = Thread.currentThread();

  8. int c = getState();

  9. if (c == 0) {

  10. // 非公平锁,直接抢占锁

  11. if (compareAndSetState(0, acquires)) {

  12. setExclusiveOwnerThread(current);

  13. return true;

  14. }

  15. }

  16. else if (current == getExclusiveOwnerThread()) {

  17. int nextc = c + acquires;

  18. if (nextc < 0) // overflow

  19. throw new Error("Maximum lock count exceeded");

  20. setState(nextc);

  21. return true;

  22. }

  23. return false;

  24. }

  25. //公平锁

  26. /**

  27. * Fair version of tryAcquire. Don't grant access unless

  28. * recursive call or no waiters or is first.

  29. */

  30. protected final boolean tryAcquire(int acquires) {

  31. final Thread current = Thread.currentThread();

  32. int c = getState();

  33. if (c == 0) {

  34. // 虽然此时此刻锁是可以用的,但是这是公平锁,既然是公平,就得讲究先来后到,

  35. // 看看有没有别人在队列中等了半天了

  36. if (!hasQueuedPredecessors() &&

  37. compareAndSetState(0, acquires)) {

  38. setExclusiveOwnerThread(current);

  39. return true;

  40. }

  41. }

  42. else if (current == getExclusiveOwnerThread()) {

  43. int nextc = c + acquires;

  44. if (nextc < 0)

  45. throw new Error("Maximum lock count exceeded");

  46. setState(nextc);

  47. return true;

  48. }

  49. return false;

  50. }

  51. }

4 ReentrantLock 释放锁

现在thread1要开始释放锁了。调用unlock方法,unlock方法又调用了内部的release方法:

  1. /**

  2. * Releases in exclusive mode. Implemented by unblocking one or

  3. * more threads if {@link #tryRelease} returns true.

  4. * This method can be used to implement method {@link Lock#unlock}.

  5. *

  6. * @param arg the release argument. This value is conveyed to

  7. * {@link #tryRelease} but is otherwise uninterpreted and

  8. * can represent anything you like.

  9. * @return the value returned from {@link #tryRelease}

  10. */

  11. public final boolean release(int arg) {

  12. if (tryRelease(arg)) {

  13. Node h = head;

  14. if (h != null && h.waitStatus != 0)

  15. unparkSuccessor(h);

  16. return true;

  17. }

  18. return false;

  19. }

  20. protected final boolean tryRelease(int releases) {

  21. int c = getState() - releases;

  22. if (Thread.currentThread() != getExclusiveOwnerThread())

  23. throw new IllegalMonitorStateException();

  24. // 是否完全释放锁

  25. boolean free = false;

  26. // 其实就是重入的问题,如果c==0,也就是说没有嵌套锁了,可以释放了,否则还不能释放掉

  27. if (c == 0) {

  28. free = true;

  29. setExclusiveOwnerThread(null);

  30. }

  31. setState(c);

  32. return free;

  33. }

调用tryRelease方法释放锁,获取当前AQS的state,并减去1,判断当前线程是否等于AQS的exclusiveOwnerThread,如果不是,就抛异常,这就保证了加锁和释放锁必须是同一个线程。如果(state-1)的结果不为0,说明锁被重入了,需要多次unlock。如果(state-1)等于0,我们就将AQS的ExclusiveOwnerThread设置为null。如果上述操作成功了,也就是tryRelase方法返回了true,那么就会判断当前队列中的head节点,当前结构如下: 
AQS实现原理分析——ReentrantLock
如果head节点不为null,并且head节点的waitStatus不为0,我们就调用unparkSuccessor方法去唤醒head节点的后继节点。

  1. /**
  2. * Wakes up node's successor, if one exists.
  3. *
  4. * @param node the node
  5. */
  6. // 唤醒后继节点
  7. // 从上面调用处知道,参数node是head头结点
  8. private void unparkSuccessor(Node node) {
  9. /*
  10. * If status is negative (i.e., possibly needing signal) try
  11. * to clear in anticipation of signalling. It is OK if this
  12. * fails or if status is changed by waiting thread.
  13. */
  14. int ws = node.waitStatus;
  15. // 如果head节点当前waitStatus<0, 将其修改为0
  16. if (ws < 0)
  17. compareAndSetWaitStatus(node, ws, 0);
  18. /*
  19. * Thread to unpark is held in successor, which is normally
  20. * just the next node. But if cancelled or apparently null,
  21. * traverse backwards from tail to find the actual
  22. * non-cancelled successor.
  23. */
  24. // 下面的代码就是唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1)
  25. // 从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的
  26. Node s = node.next;
  27. if (s == null || s.waitStatus > 0) {
  28. s = null;
  29. // 从后往前找,仔细看代码,不必担心中间有节点取消(waitStatus==1)的情况
  30. for (Node t = tail; t != null && t != node; t = t.prev)
  31. if (t.waitStatus <= 0)
  32. s = t;
  33. }
  34. if (s != null)
  35. // 唤醒线程
  36. LockSupport.unpark(s.thread);
  37. }

第一步:获取head节点的waitStatus,如果小于0,就通过CAS操作将head节点的waitStatus修改为0,现在是: 
AQS实现原理分析——ReentrantLock
第二步:寻找head节点的下一个节点,如果这个节点的waitStatus小于0,就唤醒这个节点,否则遍历下去,找到第一个waitStatus<=0的节点,并唤醒。现在thread2线程被唤醒了,我们知道刚才thread2在acquireQueued被中断,现在继续执行,又进入了for循环,当前节点的前驱节点是head并且调用tryAquire方法获得锁并且成功。那么设置当前Node为head节点,将里面的thead和prev设置为null。 
AQS实现原理分析——ReentrantLock
此时,thread2获取了锁,并成为头节点,原来的头节点释放掉,等待被回收。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
九路 九路
2年前
一行一行源码分析清楚AbstractQueuedSynchronizer
在分析Java并发包java.util.concurrent源码的时候,少不了需要了解AbstractQueuedSynchronizer(以下简写AQS)这个抽象类,因为它是Java并发包的基础工具类,是实现ReentrantLock、CountDownLatch、Semaphore、FutureTask等类的基础。Google一下A
Wesley13 Wesley13
2年前
java 面试知识点笔记(十二)多线程与并发
问:synchronized和ReentrantLock的区别?ReentrantLock(可重入锁)位于java.util.concurrent.locks包(著名的juc包是由Douglea大神写的AQS抽象类框架衍生出来的应用)和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
Wesley13 Wesley13
2年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
2年前
Java并发(八):AbstractQueuedSynchronizer
先做总结:1、AbstractQueuedSynchronizer是什么?AbstractQueuedSynchronizer(AQS)这个抽象类,是Java并发包 java.util.concurrent 的基础工具类,是实现ReentrantLock、CountDownLatch、Semaphore、FutureTask等类的
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这