2020-09-20

AQS深入理解系列(一) 独占锁的获取过程

前言

AQS(AbstractQueuedSynchronizer)可以说是整个Java并发框架的基础,其实现大量依赖乐观锁的方式(即CAS+自旋)。它实现了一个FIFO的等待队列用于等待获取同步状态,而获取/释放同步器状态的函数则依靠子类来实现。

虽然AQS是一个抽象类,但却没有任何抽象方法。如果定义为抽象方法确实不合适,因为继承使用AQS并不一定需要使用到AQS提供的所有功能(独占锁和共享锁),这样子类反而需要实现所有抽象方法。如果定义为空实现的普通方法,虽然不需要子类实现所有空方法了,但这样还是不够明确。现在AQS将这些方法的实现为抛出UnsupportedOperationException异常,那么如果是子类需要使用的方法,就覆盖掉它;如果是子类不需要使用的方法,一旦调用就会抛出异常。

JUC框架 系列文章目录

AQS实现核心

state

同步器有一个state,它代表着当前同步器的状态,它是整个AQS的核心属性。我们平时使用的JUC框架下的常用类比如ReentrantLock,其实它们的方法就是在设置和改变这个state。而之前说的子类需要实现的方法,简单的说,它的实现逻辑也就是在设置和改变这个state。

等待队列

AQS中已经为我们实现了一个FIFO的等待队列,它是一个双向链表。由于同步器的state一般不能让所有线程同时获得,所以将这些需要暂时等待的线程包装成一个节点放到队列中去,当获取state的条件满足时,会将这个节点内的线程唤醒,以便它接下来去尝试获取state。

CAS

CAS是一种轻量级的并发处理,在对AQS属性以及队列节点属性进行修改时,都会用到CAS操作。既然是多线程环境下,那么就会有多个线程同时修改这些属性,而CAS保证了同一时刻只有一个线程能修改成功,而其他CAS操作失败的线程,一般则会通过自旋继续尝试CAS操作。

根据实现核心找AQS的成员

根据上面讲到的AQS实现核心,我们来寻找AQS类中成员。

state

private volatile int state;

     
     
  • 1

如上,state已经被我们找到了,它一个volatile的变量,这样就保证了可见性。

由于本文分析的是独占锁,所以当state为0时,代表没有线程持有锁。当state为1时,代表有线程持有锁。当state>1时,代表有线程持有该锁,并且重入过该锁。所以state是否为0,可以作为判断是否有线程持有该独占锁的标准。

private transient Thread exclusiveOwnerThread;

     
     
  • 1

exclusiveOwnerThread成员则用来记录当前持有锁的线程。

等待队列

等待队列的节点需要有一个实现类,它正是Node这个静态内部类。

    static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;
    <span class="token comment">/** waitStatus value to indicate thread has cancelled */</span>
    <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">int</span> CANCELLED <span class="token operator">=</span>  <span class="token number">1</span><span class="token punctuation">;</span>
    <span class="token comment">/** waitStatus value to indicate successor's thread needs unparking */</span>
    <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">int</span> SIGNAL    <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>
    <span class="token comment">/** waitStatus value to indicate thread is waiting on condition */</span>
    <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">int</span> CONDITION <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">2</span><span class="token punctuation">;</span>
    <span class="token comment">/**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */</span>
    <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">int</span> PROPAGATE <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">3</span><span class="token punctuation">;</span>

    <span class="token keyword">volatile</span> <span class="token keyword">int</span> waitStatus<span class="token punctuation">;</span>
	<span class="token comment">// 双向链表的结构</span>
    <span class="token keyword">volatile</span> Node prev<span class="token punctuation">;</span>
    <span class="token keyword">volatile</span> Node next<span class="token punctuation">;</span>
	<span class="token comment">// Node对象用来包装线程</span>
    <span class="token keyword">volatile</span> Thread thread<span class="token punctuation">;</span>
	<span class="token comment">// 用来表明当前node的线程是想要获取共享锁还是独占锁</span>
    Node nextWaiter<span class="token punctuation">;</span>

    <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">isShared</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">return</span> nextWaiter <span class="token operator">==</span> SHARED<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 作为双向链表的节点,自然需要prevnext,即前驱和后继指针。通过这两个指针将各节点连接起来。
  • thread成员肯定也是必需的。因为Node就是用来设计包装线程对象的,自然需要一个Thread类型的成员。
  • nextWaiter成员则是用来表明当前node的线程是想要获取共享锁还是独占锁。注意,这个成员只是这个作用,不是用来连接双向链表的。
  • 最重要的还是waitStatus成员,用来表明node代表线程的状态。

有了队列节点,自然也需要AQS持有这个队列。

// 头结点,固定是一个dummy node,因为它的thread成员固定为null
private transient volatile Node head;

// 尾节点,请求锁失败的线程,会包装成node,放到队尾
private transient volatile Node tail;

  • 1
  • 2
  • 3
  • 4
  • 5

headtail都是AQS的成员,分别代表队列的头和尾,通过持有这两个成员,相当于AQS也持有了这个队列。
在这里插入图片描述
等待队列如上图所示,注意head节点作为一个dummy node,它的thread成员一定为null。

  • head节点的thread成员为null,可以理解为将它的thread成员放到AQS的exclusiveOwnerThread属性上去了,所以它的thread成员为null。
  • 即使等待线程只有一个,等待队列中的节点个数也肯定是2个,因为第一个节点总是dummy node。

CAS

熟悉原子类源码的同学,肯定都见过利用Unsafe工具类来调用CAS操作。对于AQS或Node的属性修改,也需要使用这些CAS操作。

    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;
<span class="token keyword">static</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">try</span> <span class="token punctuation">{<!-- --></span>
        stateOffset <span class="token operator">=</span> unsafe<span class="token punctuation">.</span>objectFieldOffset
            <span class="token punctuation">(</span>AbstractQueuedSynchronizer<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getDeclaredField</span><span class="token punctuation">(</span><span class="token string">"state"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        headOffset <span class="token operator">=</span> unsafe<span class="token punctuation">.</span>objectFieldOffset
            <span class="token punctuation">(</span>AbstractQueuedSynchronizer<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getDeclaredField</span><span class="token punctuation">(</span><span class="token string">"head"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        tailOffset <span class="token operator">=</span> unsafe<span class="token punctuation">.</span>objectFieldOffset
            <span class="token punctuation">(</span>AbstractQueuedSynchronizer<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getDeclaredField</span><span class="token punctuation">(</span><span class="token string">"tail"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        waitStatusOffset <span class="token operator">=</span> unsafe<span class="token punctuation">.</span>objectFieldOffset
            <span class="token punctuation">(</span>Node<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getDeclaredField</span><span class="token punctuation">(</span><span class="token string">"waitStatus"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        nextOffset <span class="token operator">=</span> unsafe<span class="token punctuation">.</span>objectFieldOffset
            <span class="token punctuation">(</span>Node<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">.</span><span class="token function">getDeclaredField</span><span class="token punctuation">(</span><span class="token string">"next"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> ex<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span>ex<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

从上面可以看到,需要通过CAS操作来修改的属性有AQS的stateheadtail;Node的waitStatusnext。因为这几个属性确实会被多线程访问。

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
private static final boolean compareAndSetWaitStatus(Node node, int expect,int update) {
    return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}
private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
    return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

最后,我们利用UnsafecompareAndSetXXX即CAS方法。

观察ReentrantLock的内部类

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
<span class="token keyword">abstract</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Sync</span> <span class="token keyword">extends</span> <span class="token class-name">AbstractQueuedSynchronizer</span> <span class="token punctuation">{<!-- --></span>
    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token punctuation">}</span>

<span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">NonfairSync</span> <span class="token keyword">extends</span> <span class="token class-name">Sync</span><span class="token punctuation">{<!-- --></span>
    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token punctuation">}</span>

<span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">FairSync</span> <span class="token keyword">extends</span> <span class="token class-name">Sync</span> <span class="token punctuation">{<!-- --></span>
    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token punctuation">}</span>
<span class="token comment">// 默认是非公平锁</span>
<span class="token keyword">public</span> <span class="token function">ReentrantLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    sync <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">NonfairSync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// 根据参数,设置公平或非公平</span>
<span class="token keyword">public</span> <span class="token function">ReentrantLock</span><span class="token punctuation">(</span><span class="token keyword">boolean</span> fair<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    sync <span class="token operator">=</span> fair <span class="token operator">?</span> <span class="token keyword">new</span> <span class="token class-name">FairSync</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token keyword">new</span> <span class="token class-name">NonfairSync</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment">// 获取锁</span>
<span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    sync<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

可以看到ReentrantLock使用AQS都是通过使用子类:

  • Sync继承了AbstractQueuedSynchronizer,在Sync里,实现了tryRelease(父类中的实现是抛出异常)。
  • NonfairSyncFairSync都继承了Sync,在NonfairSyncFairSync里,会调用acquire(父类已有的实现),会实现tryAcquire(父类中的实现是抛出异常)。
  • AQS可以实现 独占锁和共享锁,但ReentrantLock只使用了独占锁部分。获取锁的方式可以分为 公平和非公平响应中断和不响应中断。本文将以 公平、不响应中断 的方式作为切入点,来分析AQS源码。

公平的、不响应中断的 独占锁的获取

//ReentrantLock.java
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
    <span class="token keyword">final</span> <span class="token keyword">void</span> <span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token function">acquire</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token comment">/**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */</span>
    <span class="token keyword">protected</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">final</span> Thread current <span class="token operator">=</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">int</span> c <span class="token operator">=</span> <span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">hasQueuedPredecessors</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span>
                <span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> acquires<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
                <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>current<span class="token punctuation">)</span><span class="token punctuation">;</span>
                <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>current <span class="token operator">==</span> <span class="token function">getExclusiveOwnerThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
            <span class="token keyword">int</span> nextc <span class="token operator">=</span> c <span class="token operator">+</span> acquires<span class="token punctuation">;</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>nextc <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">)</span>
                <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Maximum lock count exceeded"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token function">setState</span><span class="token punctuation">(</span>nextc<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 获取锁的第一步便是调用acquire,里面的参数1代表state需要增加1。
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

先大概说一下每个函数的作用,可能的调用流程:

  1. tryAcquire用来获取独占锁一次,try的意思就是只试一次,要么成功,要么失败。
  2. tryAcquire返回true时,后面的三个函数都不会执行了。
  3. tryAcquire返回false时,必将调用addWaiteracquireQueued
  4. addWaiter是AQS的实现,因为开始获取锁失败了(tryAcquire返回false),所以需要把当前线程包装成node放到等待队列中,返回代表当前线程的node。
  5. acquireQueued是AQS的实现,简单的理解就是:用一个死循环不断得去执行tryAcquire,直到获取锁。当然:
    1. 程序不会真的在死循环,一般情况下,会不断经历 阻塞和被唤醒 两种状态。
    2. 当参数node的前驱是head时,才会去tryAcquire尝试获得锁。
  6. selfInterrupt用来复原中断状态,虽然这个版本的函数不用响应中断。当acquireQueued返回真时,代表这期间函数曾经检测到过中断状态,并且将中断状态消耗掉了(Thread.interrupted()),所以需要在退出acquire之前,将中断状态重新设置上。

悄悄给你说,其实上面就已经讲完了独占锁的获取过程了,接下来让我们具体分析。

tryAcquire

前面提到,tryAcquire在ReentrantLock里,NonfairSyncFairSync分别实现了tryAcquire,而我们这里从FairSync的tryAcquire看起。

简单来说,tryAcquire的实现就是:尝试获得锁一次,成功了返回true,否则false。

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获得当前同步器的state
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 参数acquires代表你想让同步器的state再增加多少。函数返回值为true,代表你成功获取到了锁。返回true时,同步器的state已经增加了acquires,且Thread exclusiveOwnerThread成员被设置成了当前执行线程。
  • 首先获得同步器的state。
  • 如果进入if (c == 0)分支,说明当前state为0,当前锁已经被释放掉了,但此时CHL队列中可能有线程已经在排队了(考虑到此版本是公平锁),所以需要通过hasQueuedPredecessors判断队列中是否已经有等待线程(返回true代表有)。
    • hasQueuedPredecessors返回true,那么直接返回false,说明tryAcquire失败。
    • hasQueuedPredecessors返回false,才会尝试CAS操作设置state。如果CAS操作成功,那么说明当前线程成功抢到了锁,调用setExclusiveOwnerThread设置独占线程;如果CAS操作失败,那么直接返回false,说明此次tryAcquire失败。
  • 如果进入else if (current == getExclusiveOwnerThread())分支,说明当前state不为0,已经有线程持有锁,但考虑锁可重入,如果当前线程就是AQS的独占线程的话,那么就直接让state再加acquires;如果当前线程根本不是AQS的独占线程,那么直接返回false,说明此次tryAcquire失败。

tryAcquire的逻辑总结来说:

  • 当锁已经被释放时(state为0),先以公平的方式判断是否可以尝试获得锁(hasQueuedPredecessors的返回值),再尝试获得锁(通过CAS操作设置state为1)。
  • 当锁已经被占有时,判断一下是否是自己占有的(current == getExclusiveOwnerThread()),如果是就重入这个锁。
  • 需要注意,前者设置state使用的是CAS操作,因为此时当前线程还没有获得锁,不一定谁能竞争到,所以用CAS保证只有一个线程能设置state成功;而后者直接set,是因为当前线程已经获取锁了,此时别的线程都不能修改state,即只有当前线程在写state,所以直接set是安全的。

addWaiter

既然执行到了addWaiter,说明当前线程第一次执行tryAcquire时失败了。既然获取锁失败了,那么就需要将当前线程包装一个node,放到等待队列的队尾上去,以后锁被释放时别人就会通过这个node来唤醒自己。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 使用enq的快捷方法,如果CAS操作失败,才会去执行enq 
        Node pred = tail;
        // 如果队列不为空
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 执行到这里,有两种情况:
        // 1.队列为空。head和tail成员从来没有初始化过
        // 2.CAS操作失败。当执行compareAndSetTail时,tail成员已经被修改了
        enq(node);
        return node;
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 由于当初这么调用的addWaiter(Node.EXCLUSIVE),然后会调用下面的这版构造器。由此可见,刚放到队尾的node,它的nextWaiter肯定为null,它的waitStatus为0(默认初始化)。
        static final Node EXCLUSIVE = null;
    <span class="token function">Node</span><span class="token punctuation">(</span>Thread thread<span class="token punctuation">,</span> Node mode<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>     <span class="token comment">// Used by addWaiter</span>
        <span class="token keyword">this</span><span class="token punctuation">.</span>nextWaiter <span class="token operator">=</span> mode<span class="token punctuation">;</span>
        <span class="token keyword">this</span><span class="token punctuation">.</span>thread <span class="token operator">=</span> thread<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 函数的主要逻辑,其实跟enq函数中一样,所以称它为enq的快捷方式,区别在于enq是循环执行这段逻辑。逻辑为:当队列不为空时,CAS尝试tail成员为新创建的node,如果成功,那么将next指针修正,并返回这个node。该函数既然完成了 将当前线程所包装成的node放在队尾的使命,自然就可以返回该node了。
  • 如果执行到了enq(node),那么可能有两种情况:
    • 队列为空。head和tail成员从来没有初始化过。
    • CAS操作失败。当执行compareAndSetTail时,tail成员已经被修改了。
  • 自然,enq函数能保证 在多线程下 安全得将 该node放到队尾去,它的具体做法是 自旋+CAS。
  • 不管是提前return,还是执行完enq再return,当return时,已经是将代表当前线程的node放到队尾了。注意,返回的是,代表当前线程的node。

enq

由于addWaiter的时候并没有成功将刚创建的node放到队尾,这时需要enq出马了。

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 如果tail为null,说明队列为空,head肯定也为null
            // 进一步说明 等待队列的初始化是懒汉式的
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            // 如果tail不为null,说明队列至少有一个dummy node,head肯定也不为null
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 进入if (t == null)分支,tail为null,说明队列为空,head肯定也为null。然后尝试CAS设置head成员。
    • 注意,如果队列从来没有初始化过(head、tail为null),那么这个循环至少得执行两次,第一次给队列新建一个空node,第二次进if (t == null)的else分支,把参数node放在空node的后面。
    • 根据上一条可知,就算只有一个线程入队,入队完毕后队列将有两个node,第一个node称为dummy node,因为它的thread成员为null;第二个node才算是实际意义的队头,它的thread成员不为null。
    • 新建的是空node,它的所有成员都是默认值。thread成员为null,waitStatus为0。之后你会发现,队尾node的waitStatus总是0,因为默认初始化。
    • compareAndSetHead作为一个CAS操作只有一个参数,是因为它的实现是unsafe.compareAndSwapObject(this, headOffset, null, update);
    • compareAndSetHead的CAS操作也可能失败,当队列为空时,两个线程同时执行到enq
  • 进入if (t == null)的else分支,tail不为null,说明队列至少有一个dummy node,head肯定也不为null。
    • 先令node的prev指向tail,但只有CAS设置node为tail成功后,才会将tail的next指向node,才会将双向链表的指针都正确完成指向。
    • compareAndSetTail(t, node)可能失败,当多个线程都执行到了node.prev = t这里。
  • 这个循环只有在compareAndSetTail(t, node)成功时才会退出循环,这就保证了enq最终肯定能将参数node放到队尾。
  • 简单总结就是:enq利用了自旋(循环)和CAS操作,保证了node放到队尾。

enq的尾分叉 与 prev的有效性

在队列已经有node的情况下,如果同时有多个线程执行到node.prev = t这里,最终只有一个线程能够成功设置tail成员。

            } else {
                node.prev = t;  // 1
                if (compareAndSetTail(t, node)) {  // 2
                    t.next = node;  // 3
                    return t;
                }
            }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

从上面看,发现要完成双向链表的指针指向,需要经过3步:

  1. 将参数node的前驱指向tail
  2. CAS设置参数node为tail
  3. 如果CAS成功,则修正tail的后继

因为CAS操作的保证,所有线程都能够执行第1步,但第2、3步只会有一个线程能够执行到。
在这里插入图片描述
enq的尾分叉:从上面的步骤可以看出,如果存在很多个线程都刚好执行到了node.prev = t这里,那么CAS失败的线程不能成功入队,此时它们的prev还暂时指向的旧tail。

prev的有效性:从上图第二步可以看到,此时线程1的node已经是成功放到队尾了,但此时队列却处于一个中间状态,前一个node的next还没有指向队尾呢。此时,如果另一个线程如果通过next指针遍历队列,就会漏掉最后那个node;但如果另一个线程通过tail成员的prev指针遍历队列,就不会漏掉node了。

prev的有效性也解释了AQS源码里遍历队列时,为什么常常使用tail成员和prev指针来遍历,比如你看unparkSuccessor

acquireQueued

acquireQueued函数是整个独占锁获取的精华部分,它解释了独占锁获取的整个过程。

执行到这个函数,说明:

  • 当前线程已经执行完了addWaiter方法。
  • 传入的node的thread成员就是当前线程。
  • 传入的node已经成功入队。(addWaiter的作用)

简单的说,acquireQueued是利用自旋+tryAcquire“不断”地尝试获取锁,当然实际上如果真的是在死循环来不断调tryAcquire,肯定电脑都卡死了。实际流程是:

  • 每次循环都会判断是否可以尝试获取锁(p == head),如果可以,那么尝试(tryAcquire(arg))。
    • 如果尝试获取锁成功,那么函数的使命就达到了,执行完相应收尾工作,然后返回。
  • 如果 不可以尝试 或者 尝试获取锁却失败了,那么阻塞当前线程(parkAndCheckInterrupt)。
  • 如果当前线程被唤醒了,又会重新走这个流程。被唤醒时,是从parkAndCheckInterrupt处唤醒,然后从这里继续往下执行。
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // 尝试获取锁的前提是node是head的后继
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 执行到这里,说明
                // 要么不符合 尝试获取锁的前提
                // 要么 尝试获取锁失败了
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

为什么tryAcquire(arg)的前提是p == head
因为从enq的逻辑可知,head只会是一个dummy node,实际意义的node只会在head的后面。而node的前驱是head(final Node p = node.predecessor()),则代表node已经是队列中的第一个实际node了,排在最前面的node自然可以尝试去获取锁了。

回想整个调用过程,是最开始在acquire里调用tryAcquire就已经失败了,然而此时第一次循环时,又可能马上去调tryAcquire(说可能,是因为需要p == head成立),这会不会是一次肯定失败的tryAcquire
考虑这种场景,线程1获取了锁正在使用还没释放,此时队列为空,线程2此时也来获取锁,自然最开始在acquire里调用tryAcquire会失败,假设线程2刚开始执行acquireQueued,此时线程1释放了锁,此时线程2肯定排在head后面,那么线程2马上tryAcquire,然后就可以获取成功。

执行acquireQueued的线程是谁?
一定是node参数的thread成员,虽然执行过程中,可能会经历不断 阻塞和被唤醒 的过程。

为什么刚执行完addWaiter方法时,才把代表当前线程的node放到队尾,怎么之后一判断就会发现自己处于head的后继了?
这个问题不考虑上面的特殊场景,而考虑addWaiter时,队列中有许多node。其实这很合理,这说明从head到当前方法栈中的node之间的那些node,它们自己也会在执行acquireQueued,它们依次执行成功(指p == head && tryAcquire(arg)成功),每次执行成功相当于把head成员从队列上后移一个node,当它们都执行完毕,当前方法栈中的node自然也就是head的后继了。
注意,“之间的那些node”的最后一个node执行acquireQueued成功后(代表 最后一个node的代表线程获得锁成功,它自己成为了head),当前方法还在阻塞之中,只有当这“最后一个node”释放独占锁时,才会执行unparkSuccessor(head),当前线程才会被唤醒。关于释放独占锁,以后再具体讲。

finally块是否会执行cancelAcquire(node)
虽然号称此函数是不响应中断的函数,但不响应中断只是对于AQS的使用者来说,如果一个线程阻塞在parkAndCheckInterrupt这里,别的线程来中断它,它是会马上唤醒的,然后继续这个循环。不过想要退出这个函数,只有通过return interrupted,而前一句就是failed = false,所以finally块里,是永远不会去执行cancelAcquire(node)的。

如下图,演示了多个线程依次获得独占锁的过程。
在这里插入图片描述

                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
<span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">setHead</span><span class="token punctuation">(</span>Node node<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    head <span class="token operator">=</span> node<span class="token punctuation">;</span>
    node<span class="token punctuation">.</span>thread <span class="token operator">=</span> null<span class="token punctuation">;</span>
    node<span class="token punctuation">.</span>prev <span class="token operator">=</span> null<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

从逻辑中可见,如果当前线程获取锁成功,代表当前线程的node会新设置自己为head,然后将其弄成dummy node,即把node的thread成员清空,但这个被清空的thread成员已经在tryAcquire里将这个thread设置为了exclusiveOwnerThread。

  • 可以这么理解,head之所以它的thread成员为null,是因为它的thread成员被放在了exclusiveOwnerThread成员里。
  • 使用setHead而非compareAndSetHead,因为此时不需要CAS操作,执行到这里说明当前线程已经获得了独占锁(tryAcquire成功),所以别的线程是不可能同时执行这部分代码的。

shouldParkAfterFailedAcquire

我们先把函数名分成两部分:

shouldPark:指的是 该函数的返回值影响是否可以执行parkAndCheckInterrupt函数。因为shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()这么写,只有当该函数返回true时,才会去执行parkAndCheckInterrupt。注意,parkAndCheckInterrupt函数执行后,当前线程就会被挂起了,也就是我们所说的阻塞。

AfterFailedAcquire:指的是 获取锁失败了才会执行该函数。其实具体指两种情况:1. p == head为false,即当前线程的node的前驱不是head 2. 虽然 p == head为true,但parkAndCheckInterrupt返回false了,即当前线程虽然已经排到等待队列的最前面,但获取锁还是失败了。

回想之前讲过的函数,发现它们居然都没有使用过waitStatus这个属性,而现在就可以派上用场了。

static final int CANCELLED =  1;
static final int SIGNAL    = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;

 
 
  • 1
  • 2
  • 3
  • 4

node一共有四种状态,但在独占锁的获取和释放过程中,我们只可能将node的状态变成CANCELLEDSIGNAL,而shouldParkAfterFailedAcquire函数就会把一个node的状态变成SIGNAL。注意,一个node新建的时候,它的waitStatus是默认初始化为0的。

  • CANCELLED,一个node的状态是CANCELLED,说明这个node的代表线程已经取消等待了。
  • SIGNAL,一个node的状态是SIGNAL,说明这个node的后继node的代表线程已经阻塞或马上阻塞(shouldParkAfterFailedAcquire设置前驱为SIGNAL后,下一次的acquireQueued循环可能就会阻塞了,所以说“已经阻塞或马上阻塞”),并且当前node成为head并释放锁时,会根据SIGNAL来唤醒后继node。即SIGNAL是唤醒后继节点的标志。
  • 有趣的是,CANCELLED状态与当前node关联,SIGNAL状态与后继node关联。

本函数,简单的讲就是,跳过无效前驱,把node的有效前驱(有效是指node不是CANCELLED的)找到,并且将有效前驱的状态设置为SIGNAL,之后便返回true代表马上可以阻塞了。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点已经设置了SIGNAL,闹钟已经设好,现在我可以安心睡觉(阻塞)了。
             * 如果前驱变成了head,并且head的代表线程exclusiveOwnerThread释放了锁,
             * 就会来根据这个SIGNAL来唤醒自己
             */
            return true;
        if (ws > 0) {
            /*
             * 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断,
             * 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 进入这个分支,ws只能是0或PROPAGATE。
             * CAS设置ws为SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 如果前驱节点的状态为SIGNAL,说明闹钟标志已设好,返回true表示设置完毕。
  • 如果前驱节点的状态为CANCELLED,说明前驱节点本身不再等待了,需要跨越这些节点,然后找到一个有效节点,再把node和这个有效节点的前驱后继连接好。
  • 如果是其他情况,那么CAS尝试设置前驱节点为SIGNAL

由于shouldParkAfterFailedAcquire函数在acquireQueued的调用中处于一个死循环中,且因为shouldParkAfterFailedAcquire函数若返回false,且考虑当前线程一直不能获取到锁的情况,那么此函数必将至少执行两次才能阻塞自己。

  • shouldParkAfterFailedAcquire只有在检测到前驱的状态为SIGNAL才能返回true,只有true才会执行到parkAndCheckInterrupt
  • shouldParkAfterFailedAcquire返回false后,进入下一次循环,当前线程又会再次尝试获取锁(p == head && tryAcquire(arg))。或者说,每次执行shouldParkAfterFailedAcquire,都说明当前循环 尝试过获取锁了,但失败了。
  • 如果刚开始前驱的状态为0,那么需要第一次执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL)返回false并进入下一次循环,第二次才能进入if (ws == Node.SIGNAL)分支,所以说 至少执行两次。
  • 死循环保证了最终一定能设置前驱为SIGNAL成功的。(考虑当前线程一直不能获取到锁)

parkAndCheckInterrupt

既然shouldParkAfterFailedAcquire已经返回了true,那么我现在可以执行parkAndCheckInterrupt了。

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);  //阻塞当前线程
        return Thread.interrupted();
    }

 
 
  • 1
  • 2
  • 3
  • 4

调用完LockSupport.park(this),当前线程就阻塞在这里,直到有别的线程unpark了当前线程,或者中断了当前线程。而返回的Thread.interrupted()代表当前线程在阻塞的过程中,有没有被别的线程中断过,如果有,则返回true。注意,Thread.interrupted()会消耗掉中断的状态,即第一次执行能返回true,但紧接着第二次执行就只会返回false了。

  • 如果是别的线程unpark了当前线程,那么调用Thread.interrupted()返回false。
  • 如果是别的线程中断了当前线程,那么调用Thread.interrupted()返回true。

回到acquireQueued的逻辑中,发现一旦当前线程被中断过一次,那么parkAndCheckInterrupt就返回了true,那么执行interrupted = trueinterrupted局部变量就一定是true的了。

    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)) {
                    // 获取锁成功后
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())  //这里返回了true,说明上一行也返回了true才会执行到这里
                    interrupted = true;  //这里设置后,会被永久保留
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

再等到当前线程获取锁成功后,那么acquireQueued返回的就一定是true了。再回到acquire的逻辑,发现需要进入if分支,再执行selfInterrupt()将中断状态补上,这样下一次Thread.interrupted()就能返回true了。为的就是在 回到用户代码之前,把中断状态补上,万一用户需要中断状态呢。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  //acquireQueued返回了true
            selfInterrupt(); //中断状态补上
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

不公平的、不响应中断的 独占锁的获取

其实不公平锁与公平锁之间的差别只会体现在AQS子类的重写方法中,所以我们直接看子类实现就好。使用不公平锁对ReentrantLock来说,还是直接调用ReentrantLock.lock(),没有区别。要想使用非公平锁,必须在构造ReentrantLock时指定好。

观察ReentrantLock的内部类

//ReentrantLock.java
    abstract static class Sync extends AbstractQueuedSynchronizer {
    	...
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
        ...
	}
<span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">NonfairSync</span> <span class="token keyword">extends</span> <span class="token class-name">Sync</span> <span class="token punctuation">{<!-- --></span>
    <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> <span class="token keyword">long</span> serialVersionUID <span class="token operator">=</span> <span class="token number">7316153563782823691</span>L<span class="token punctuation">;</span>

    <span class="token keyword">final</span> <span class="token keyword">void</span> <span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    	<span class="token comment">// if分支里的逻辑只是一次快速尝试</span>
    	<span class="token comment">// 它和nonfairTryAcquire里的部分逻辑是一样的</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">else</span>
            <span class="token function">acquire</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token keyword">protected</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
    	<span class="token comment">// 将nonfairTryAcquire的逻辑直接放在这里,就把公平锁看起来一样了</span>
        <span class="token keyword">return</span> <span class="token function">nonfairTryAcquire</span><span class="token punctuation">(</span>acquires<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 与公平锁相比,非公平锁的lock实现略有不同。公平锁的lock实现是直接调用acquire(1),而非公平锁的lock实现会先尝试CAS修改state,如果能够将state从0改成1,那么说明当前线程获取锁,既然获取锁,那么便直接插队setExclusiveOwnerThread(Thread.currentThread())。如果CAS操作失败,再走正常流程,调用父类函数acquire(1)
  • 再看子类实现nonfairTryAcquire,发现和公平锁的实现tryAcquire几乎一模一样,除了公平锁需要多判断一下hasQueuedPredecessors的返回值,也就是需要判断队列是否等待线程。

其实非公平锁的原理就是:不管等待队列中是否有等待线程,直接竞争获取state,要是获取成功了,就直接设置当前线程为exclusiveOwnerThread成员了。这不就是,插了所有等待线程的队嘛。也就是说,非公平锁它可以和队列中的head后继的代表线程同时竞争锁。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&  // 非公平锁插队的机会,只有这里tryAcquire的时候
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5

但是 非公平锁插队的机会只有在acquire里面第一次执行tryAcquire的时候,一旦这里tryAcquire获取锁失败了,就会进入acquireQueued的死循环,在循环之前会将当前线程包装成node放到队尾去,之后在循环中的每次循环,想要执行获取锁的动作(tryAcquire(arg))必须自己是head的后继才可以(p == head)。

总结:

  • 非公平锁插队的机会只有,acquire方法里第一次执行tryAcquire的时候,如果这次插队失败,那么它也不可能插队成功了。
  • 公平锁和非公平锁,在进入acquireQueued之后,使用起来没有任何区别。

响应中断的 独占锁的获取

使用响应中断的锁对ReentrantLock来说,是调用ReentrantLock.lockInterruptibly()。从下面函数中,可以看到函数都会抛出InterruptedException异常,这是我们能看到的第一处不同。

//ReentrantLock.java
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
//AbstractQueuedSynchronizer.java
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

acquireInterruptibly这个方法与 不响应中断的acquire方法 对应。同样的,进入这个方法后,会第一次进行tryAcquire尝试。但不同的,此acquireInterruptibly函数中,会去检测Thread.interrupted(),并抛出异常。

但注意,对于acquireInterruptibly这个方法而言,既可以是公平的,也可以是不公平的,这完全取决于tryAcquire的实现(即取决于ReentrantLock当初是怎么构造的)。

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

doAcquireInterruptibly方法 的相对应的方法为acquireQueued方法。观察两个方法的差别很少:

  • doAcquireInterruptibly没有返回值,而acquireQueued方法有返回值,代表当前线程是否被中断过。
    • acquireQueued方法需要有返回值,因为函数调用返回上层后,需要根据返回值来判断是否需要重新设置中断状态,在返回用户代码之前。
    • doAcquireInterruptibly不需要返回值,因为该函数中如果检测到了中断状态,就直接抛出异常就好了。
  • doAcquireInterruptibly方法的finally块是可能会执行到cancelAcquire(node)的,而acquireQueued方法不可能去执行cancelAcquire(node)的。
    • doAcquireInterruptibly方法中,如果线程阻塞在parkAndCheckInterrupt这里后,别的线程来中断阻塞线程,阻塞线程会被唤醒,然后抛出异常。本来抛出异常该函数就马上结束掉的,但由于有finally块,所以在结束掉之前会去执行finally块,并且由于failed为true,则会执行cancelAcquire(node)

回忆起node有一种CANCELLED状态,看来就是为cancelAcquire(node)准备的了。

    private void cancelAcquire(Node node) {
        // 避免空指针异常
        if (node == null)
            return;
    node<span class="token punctuation">.</span>thread <span class="token operator">=</span> null<span class="token punctuation">;</span>
    
    Node pred <span class="token operator">=</span> node<span class="token punctuation">.</span>prev<span class="token punctuation">;</span>
    <span class="token keyword">while</span> <span class="token punctuation">(</span>pred<span class="token punctuation">.</span>waitStatus <span class="token operator">&gt;</span> <span class="token number">0</span><span class="token punctuation">)</span>  <span class="token comment">// 循环用来跳过无效前驱</span>
        node<span class="token punctuation">.</span>prev <span class="token operator">=</span> pred <span class="token operator">=</span> pred<span class="token punctuation">.</span>prev<span class="token punctuation">;</span>
    <span class="token comment">// 执行完循环,pred会指向node的有效前驱。</span>
    <span class="token comment">// 当然,如果node的前驱就是有效的。那么就不需要跳过了。</span>

    <span class="token comment">// pred的后继无论如何都需要取消,因为即使前面循环没有执行,</span>
    <span class="token comment">// 现在pred的后继(肯定是参数node)也是一个马上取消掉的node。</span>
    <span class="token comment">// 之后有些CAS操作会尝试修改pred的后继,如果CAS失败,那么说明有别的线程在做</span>
    <span class="token comment">// 取消动作或通知动作,所以当前线程也不需要更多的动作了。</span>
    Node predNext <span class="token operator">=</span> pred<span class="token punctuation">.</span>next<span class="token punctuation">;</span>

    <span class="token comment">// 这里直接使用赋值操作,而不是CAS操作。</span>
    <span class="token comment">// 如果别的线程在执行这步之后,别的线程将会跳过这个node。</span>
    <span class="token comment">// 如果别的线程在执行这步之前,别的线程还是会将这个node当作有效节点。</span>
    node<span class="token punctuation">.</span>waitStatus <span class="token operator">=</span> Node<span class="token punctuation">.</span>CANCELLED<span class="token punctuation">;</span>

    <span class="token comment">// 如果node是队尾,那简单。直接设置pred为队尾,然后设置pred的后继为null</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>node <span class="token operator">==</span> tail <span class="token operator">&amp;&amp;</span> <span class="token function">compareAndSetTail</span><span class="token punctuation">(</span>node<span class="token punctuation">,</span> pred<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token function">compareAndSetNext</span><span class="token punctuation">(</span>pred<span class="token punctuation">,</span> predNext<span class="token punctuation">,</span> null<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token comment">// 如果node不是队尾,node则有后继。</span>
    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">int</span> ws<span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>pred <span class="token operator">!=</span> head <span class="token operator">&amp;&amp;</span>
            <span class="token punctuation">(</span><span class="token punctuation">(</span>ws <span class="token operator">=</span> pred<span class="token punctuation">.</span>waitStatus<span class="token punctuation">)</span> <span class="token operator">==</span> Node<span class="token punctuation">.</span>SIGNAL <span class="token operator">||</span>
             <span class="token punctuation">(</span>ws <span class="token operator">&lt;=</span> <span class="token number">0</span> <span class="token operator">&amp;&amp;</span> <span class="token function">compareAndSetWaitStatus</span><span class="token punctuation">(</span>pred<span class="token punctuation">,</span> ws<span class="token punctuation">,</span> Node<span class="token punctuation">.</span>SIGNAL<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span>
            pred<span class="token punctuation">.</span>thread <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
            Node next <span class="token operator">=</span> node<span class="token punctuation">.</span>next<span class="token punctuation">;</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>next <span class="token operator">!=</span> null <span class="token operator">&amp;&amp;</span> next<span class="token punctuation">.</span>waitStatus <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
                <span class="token function">compareAndSetNext</span><span class="token punctuation">(</span>pred<span class="token punctuation">,</span> predNext<span class="token punctuation">,</span> next<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{<!-- --></span>
            <span class="token function">unparkSuccessor</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>

        node<span class="token punctuation">.</span>next <span class="token operator">=</span> node<span class="token punctuation">;</span> <span class="token comment">// help GC</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

如果node是队尾,那说明node后面没有节点,也就不存在节点需要去唤醒了。

分析node不是队尾的情况,既然不是队尾,说明node后面有节点。如下图所示。
这里面又有分支,先直接看进入分支要做什么,再来分析进入分支的条件:

  • 在if分支里,去做compareAndSetNext(pred, predNext, next),即修复pred的后继,完善双向链表的指针。但这样做有一个前提,就是需要pred的状态已经是SIGNAL的了。
  • 在else分支里,去做unparkSuccessor(node),去唤醒node后继。

在这里插入图片描述
从上图可以看到,实际意义上,pred的后继已经变成了node后继了。

  • pred != head不成立,那么说明pred就是head,执行else分支。node的后继运气爆棚,因为node自己取消掉了,node的后继便成为了等待队列中第一个线程(即成为了head后继),自然需要去唤醒它了(unparkSuccessor(node))。
  • pred != head成立,但(ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))不成立,执行else分支。
    • 第一个条件成立,说明pred并不是head。
    • 第二个条件不成立,里面是||,说明是两个子条件都不成立。有下面情况:
      • 如果pred的状态是CANCELLED。这似乎有点矛盾,因为之前的for循环已经判断过pred的状态是 <= 0的了,但你要知道多线程环境下,什么都可能发生。很可能从for循环执行到这里的时候,pred又被取消掉了。考虑到pred如果是head后继的话,那么node后继就一下子成为了队列中第一个线程了,所以还是有必要执行unparkSuccessor(node)
      • 如果pred的状态是 <= 0但还不是SIGNAL,但CAS设置pred的状态为SIGNAL却失败了。SIGNAL是作为后继节点被唤醒的标志而存在的,现在居然没有设置成功,所以很有必要执行unparkSuccessor(node)
  • pred != head成立,(ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))也成立,但pred.thread != null不成立,执行else分支。
    • 第一个条件成立,说明pred并不是head。
    • 第二个条件成立,里面是||,说明是两个子条件要么前真,要么前假后真。
      • 如果是前真,那么说明pred的状态已经是SIGNAL。
      • 如果是前假后真,那么说明pred的状态是0或PROPAGATE,且接下来的CAS操作也成功了,即成功设置为SIGNAL。
    • 第三个条件不成立,说明pred变成了一个dummy node了。要么pred是变成head了,要么pred突然被取消了(执行了node.thread = null)。这两种情况,前者必须执行unparkSuccessor(node),后者只是有必要执行。
  • pred != head成立,(ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))也成立,且pred.thread != null也成立,只有这里才执行if分支。说明闹钟设置成功,并且设置的node也不是个空节点,那么只需要将pred与node后继相连即可(compareAndSetNext(pred, predNext, next))。

总结一下cancelAcquire

  • 如果闹钟设置成功,且闹钟都在node不是一个dummy node,那么将pred与node后继相连,让node后继继续安心睡觉。
  • 如果发现node后继已经是排队的第一个了、发现闹钟没有设置成功、发现闹钟设置成功但闹钟所在节点变空节点,都需要换线node后继。

超时的 独占锁的获取

使用超时的锁对ReentrantLock来说,是调用public boolean tryLock(long timeout, TimeUnit unit)。从下面函数中,可以看到函数都会抛出InterruptedException异常,这是我们能看到的第一处不同。

//ReentrantLock.java
    public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));//第二个实参,换算得到nano
    }
//AbstractQueuedSynchronizer.java
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

tryAcquireNanos这个方法与 不响应中断的acquire方法 对应。同样的,进入这个方法后,会第一次进行tryAcquire尝试。但不同的,此tryAcquireNanos函数中,会先去检测Thread.interrupted(),并抛出异常。

但注意,对于tryAcquireNanos这个方法而言,既可以是公平的,也可以是不公平的,这完全取决于tryAcquire的实现(即取决于ReentrantLock当初是怎么构造的)。

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)//如果时间小于0,直接返回失败
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;//算出“死期”,当死期到了就直接返回失败了
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                // 看看你的年龄是否到达了死期
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)//小于0,说明死期到了
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)//剩余寿命得大于1ms
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

doAcquireNanos方法 与不响应中断的acquireQueued方法、响应中断的doAcquireInterruptibly方法进行对比,发现差别很少,简单的我用注释说明了。

  • 每次循环都会检查时间是否到达deadline。
  • 当剩余时间小于spinForTimeoutThreshold时,则不能调用LockSupport.parkNanos,因为时间太短,反而无法精确控制阻塞时间,所以不如在剩余的时间里一直循环。
  • LockSupport.parkNanos除了会因为别人的park而唤醒,也会因为别人的中断而唤醒,当然最重要的,时间到了,它自己会唤醒。
  • 不管哪种情况,被唤醒后,都会检查中断状态。每个循环都会检查一次。

只一次尝试的 非公平的 独占锁的获取

只一次尝试的、非公平的独占锁获得对ReentrantLock来说,是调用ReentrantLock.tryLock()。而nonfairTryAcquire方法就是之前讲过的非公平锁的获取方式。

//ReentrantLock.java
public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
<span class="token keyword">abstract</span> <span class="token keyword">static</span> <span class="token keyword">class</span> <span class="token class-name">Sync</span> <span class="token keyword">extends</span> <span class="token class-name">AbstractQueuedSynchronizer</span> <span class="token punctuation">{<!-- --></span>
    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
    <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">nonfairTryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
        <span class="token keyword">final</span> Thread current <span class="token operator">=</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">int</span> c <span class="token operator">=</span> <span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> acquires<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
                <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>current<span class="token punctuation">)</span><span class="token punctuation">;</span>
                <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>current <span class="token operator">==</span> <span class="token function">getExclusiveOwnerThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{<!-- --></span>
            <span class="token keyword">int</span> nextc <span class="token operator">=</span> c <span class="token operator">+</span> acquires<span class="token punctuation">;</span>
            <span class="token keyword">if</span> <span class="token punctuation">(</span>nextc <span class="token operator">&lt;</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token comment">// overflow</span>
                <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Maximum lock count exceeded"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token function">setState</span><span class="token punctuation">(</span>nextc<span class="token punctuation">)</span><span class="token punctuation">;</span>
            <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token punctuation">}</span>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

总结

  • 对于独占锁,AQS的state代表代表锁的状态,为0代表没有线程持有锁,非0代表有线程持有了锁。
  • 获得了锁的线程会将自己设置为exclusiveOwnerThread
  • addWaiter负责new出一个包装当前线程的node,enq负责将node添加到队尾,如果队尾为空,它还负责添加dummy node。
  • acquireQueued是整个获取锁过程的核心,这里是指它的那个死循环。一般情况下,每次循环做的事就是:尝试获取锁,获取锁失败,阻塞,被唤醒。如果某一次循环获取锁成功,那么之后会返回到用户代码调用处。
  • shouldParkAfterFailedAcquire负责自身的前驱节点的状态设置为SIGNAL,这样,当前驱节点释放锁时,会根据SIGNAL来唤醒自身。
  • parkAndCheckInterrupt最简单,用来阻塞当前线程。它也会去检查中断状态。
在linux下抓取目录树,双击后 获取该节点子节点(逐步生成)。另外有两个类,一个是windows下的(一次性 获取目录树),一个是linux下的(足部 获取目录树)
这是梁顺林的定量遥感的中文版,由范闻捷等翻译的,是电子版PDF,解决了大家看英文费时费事的问题,希望大家下载看看,一定会有帮助的
		<textarea class="comment-content" name="comment_content" id="comment_content" placeholder="优质评论可以帮助作者获得更高权重" maxlength="1000"></textarea>
		<div class="opt-box">
			<div id="ubbtools" class="add_code">
				<a href="#insertcode" code="code" target="_self"><i class="icon iconfont icon-daima"></i></a>
			</div>
			<input type="hidden" id="comment_replyId" name="comment_replyId">
			<input type="hidden" id="article_id" name="article_id" value="106344926">
			<input type="hidden" id="comment_userId" name="comment_userId" value="">
			<input type="hidden" id="commentId" name="commentId" value="">
			<div class="dropdown" id="myDrap">
				<a class="dropdown-face d-flex align-items-center" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
					<div class="txt-selected text-truncate">添加代码片</div>
					<svg class="icon d-block" width="200px" height="100.00px" viewBox="0 0 2048 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M597.33333292 298.666667h853.333334L1023.99999992 725.333333 597.33333292 298.666667z"></path></svg>
				</a>
				<ul class="dropdown-menu" id="commentCode" aria-labelledby="drop4">
					<li><a data-code="html">HTML/XML</a></li>
					<li><a data-code="objc">objective-c</a></li>
					<li><a data-code="ruby">Ruby</a></li>
					<li><a data-code="php">PHP</a></li>
					<li><a data-code="csharp">C</a></li>
					<li><a data-code="cpp">C++</a></li>
					<li><a data-code="javascript">JavaScript</a></li>
					<li><a data-code="python">Python</a></li>
					<li><a data-code="java">Java</a></li>
					<li><a data-code="css">CSS</a></li>
					<li><a data-code="sql">SQL</a></li>
					<li><a data-code="plain">其它</a></li>
				</ul>
			</div>
			<div class="right-box" id="rightBox" data-type="2">
						<span id="tip_comment" class="tip">还能输入<em>1000</em>个字符</span>
						<a data-report-click="{&quot;spm&quot;:&quot;3001.4374&quot;}" class="btn btn-sm btn-quick-comment" id="quickComment">“速评一下”</a>
						<a data-report-click="{&quot;mod&quot;:&quot;1582594662_003&quot;,&quot;spm&quot;:&quot;1001.2101.3001.4227&quot;,&quot;ab&quot;:&quot;new&quot;}"><input type="submit" class="btn btn-sm btn-comment" value="发表评论"></a>
			</div>
		</div>
	</form>
	<input type="button" class="bt-comment-show" value="评论">
</div>
<div class="comment-list-container" style="display: block;">
	<a id="comments"></a>
	<div class="comment-list-box"><ul class="comment-list"><li class="comment-line-box d-flex" data-commentid="13045334" data-replyname="weixin_43914493"><div style="display: flex;width: 100%;">      <a target="_blank" href="https://me.csdn.net/weixin_43914493"><img src="https://profile.csdnimg.cn/C/A/9/3_weixin_43914493" username="weixin_43914493" alt="weixin_43914493" class="avatar"></a>        <div class="right-box ">          <div class="new-info-box clearfix">            <a target="_blank" href="https://me.csdn.net/weixin_43914493"><span class="name ">酱天小禽兽</span></a><span class="colon">:</span><span class="floor-num"></span><span class="new-comment">太棒哩!</span><span class="date" title="2020-08-14 14:04:43">1月前</span><span class="new-opt-floating"><a class="btn-bt  btn-reply" data-type="reply" data-flag="true">回复</a><a class="btn-bt  btn-report"><img class="btn-report-img" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentMore.png" title=""><span data-type="report" class="hide-report">举报</span></a></span></div><div class="comment-like " data-commentid="13045334"><img class="comment-like-img unclickImg" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentUnHeart.png" title="点赞"><img class="comment-like-img comment-like-img-hover" style="display:none" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentActiveHeart.png" title="点赞"><img class="comment-like-img clickedImg" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentActiveHeart.png" title="点赞"><span></span></div></div></div></li></ul><ul class="comment-list"><li class="comment-line-box d-flex" data-commentid="12523192" data-replyname="u012688497"><div style="display: flex;width: 100%;">      <a target="_blank" href="https://me.csdn.net/u012688497"><img src="https://profile.csdnimg.cn/7/4/A/3_u012688497" username="u012688497" alt="u012688497" class="avatar"></a>        <div class="right-box ">          <div class="new-info-box clearfix">            <a target="_blank" href="https://me.csdn.net/u012688497"><span class="name ">liuqiqi0990</span></a><span class="colon">:</span><span class="floor-num"></span><span class="new-comment">写的很详细,谢谢分享</span><span class="date" title="2020-06-12 20:13:57">3月前</span><span class="new-opt-floating"><a class="btn-bt  btn-reply" data-type="reply" data-flag="true">回复</a><a class="btn-bt  btn-report"><img class="btn-report-img" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentMore.png" title=""><span data-type="report" class="hide-report">举报</span></a></span></div><div class="comment-like " data-commentid="12523192"><img class="comment-like-img unclickImg" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentUnHeart.png" title="点赞"><img class="comment-like-img comment-like-img-hover" style="display:none" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentActiveHeart.png" title="点赞"><img class="comment-like-img clickedImg" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentActiveHeart.png" title="点赞"><span></span></div></div></div></li><li class="replay-box" style="display:block"><ul class="comment-list"><li class="comment-line-box d-flex" data-commentid="12526665" data-replyname="anlian523"><div style="display: flex;width: 100%;">      <a target="_blank" href="https://me.csdn.net/anlian523"><img src="https://profile.csdnimg.cn/9/9/2/3_anlian523" username="anlian523" alt="anlian523" class="avatar"></a>        <div class="right-box ">          <div class="new-info-box clearfix">            <a class="comment-tag" target="_blank" href="https://blog.csdn.net/blogdevteam/article/details/103478461">码皇<img class="comment-tag-img" src="https://csdnimg.cn/release/blogv2/dist/components/img/commentTagArrowWhite.png" title="码皇"></a><a target="_blank" href="https://me.csdn.net/anlian523"><span class="name mr-8">anlian523<img class="is_bloger" src="https://csdnimg.cn/release/blogv2/dist/components/img/bloger@2x.png" "=""></span></a><span class="text">回复</span><span class="colon">:</span><span class="text"></span><span class="new-comment">谢谢支持!</span><span class="date" title="2020-06-13 11:55:03">3月前</span><span class="new-opt-floating"><a class="btn-bt  btn-reply" data-type="reply" data-flag="true">回复</a><a class="btn-bt  btn-report"><img class="btn-report-img" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentMore.png" title=""><span data-type="report" class="hide-report">举报</span></a></span></div><div class="comment-like " data-commentid="12526665"><img class="comment-like-img unclickImg" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentUnHeart.png" title="点赞"><img class="comment-like-img comment-like-img-hover" style="display:none" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentActiveHeart.png" title="点赞"><img class="comment-like-img clickedImg" src="https://csdnimg.cn/release/blogv2/dist/pc/img/commentActiveHeart.png" title="点赞"><span></span></div></div></div></li></ul></li></ul></div>
	<div id="commentPage" class="pagination-box d-none" style="display: block;"><div id="Paging_020213049860808963" class="ui-paging-container"><ul><li class="js-page-first js-page-action ui-pager ui-pager-disabled"></li><li class="js-page-prev js-page-action ui-pager ui-pager-disabled">&lt;</li><li data-page="1" class="ui-pager focus">1</li><li class="js-page-next js-page-action ui-pager ui-pager-disabled">&gt;</li><li class="js-page-last js-page-action ui-pager ui-pager-disabled"></li></ul></div></div>
	
</div>
开场白 AQS在juc包中简直是基石般的存在,笔者会通过juc包中的ReentrantLock来讲解 AQS独占锁实现,通过Semaphore来讲解下 AQS共享锁的实现。 本文力求用直白的结构图和详细的描述,让大家花最少的时间,便能够比较详细的了解 AQS的流程。一、 AQS等待队列 所有未 获取到锁的线程,都会进入 AQS的等待队列,其实就是一个双向链表,如下图: ...
私下里,有不少读者问我:“二哥,如何才能写出一份专业的技术简历呢?我总感觉自己写的简历太烂了,所以投了无数份,都石沉大海了。”说实话,我自己好多年没有写过简历了,但我认识的一个同行,他在阿里,给我说了一些他当年写简历的方法论,我感觉太牛逼了,实在是忍不住,就分享了出来,希望能够帮助到你。 01、简历的本质 作为简历的撰写者,你必须要搞清楚一点,简历的本质是什么,它就是为了来销售你的价值主张的。往深...
深入剖析基于并发 AQS的( 独占锁)重入锁(ReetrantLock)及..._CSDN博客
9-18
publicinterfaceLock{//加锁voidlock();//解锁voidunlock();//可中断 获取锁,与lock()不同之处在于可响应中断操作,即在获//取锁的 过程中可中断,注意synchronized...
AQS 深入 理解 系列(三)共享锁的 获取与释放_anlian523的博客-CSDN博客
9-12
在前面两篇 系列文章中,已经讲解了 独占锁获取和释放 过程,而共享锁的 获取与释放 过程也很类似,如果你前面 独占锁的内容都看懂了,那么共享锁你也就触类旁通了。 JUC...
不知道是不是只有我这样子,还是你们也有过类似的经历。 上学的时候总有很多光辉历史,学年名列前茅,或者单科目大佬,但是虽然慢慢地长大了,你开始懈怠了,开始废掉了。。。 什么?你说不知道具体的情况是怎么样的? 我来告诉你: 你常常潜意识里或者心理觉得,自己真正的生活或者奋斗还没有开始。总是幻想着自己还拥有大把时间,还有无限的可能,自己还能逆风翻盘,只不是自己还没开始罢了,自己以后肯定会变得特别厉害...
ReentrantReadWriteLock 与 ReentrantLock 核心就是Sync( AQS子类)及 AQS,Synchronized基于内部对象锁JVM指令的级别

ReentrantReadWriteLock 引用传入 内部类 ReadLock和WriteLock,将其Sync传入两个内部类,如果读锁如果发现没有排他锁exclusiveCount则可以上锁
 
Reent


从源码的角度 深入 理解 AQS加锁解锁 过程_了不起的盖茨比..._CSDN博客
9-13
),不得不让我们想起多线程的同步的几种的实现方式,主要是wait/notify,synchronized,ReentrantLock,下面我们会介绍几种同步的实现方式,从而推理出 AQS的加锁的 过程
AQS 深入 理解 系列(二) 独占锁的释放 过程_anlian523的博..._CSDN博客
8-5
在上一篇文章 AQS 深入 理解 独占锁获取 过程中,我们分析了 独占锁获取 过程。相对于 获取 过程, 独占锁的释放 过程则相对简单了很多。 Lock lock = new ReentrantLock()...
一、简介 Java语言中有许多原生线程安全的数据结构,比如ArrayBlockingQueue、CopyOnWriteArrayList、LinkedBlockingQueue,它们线程安全的实现方式并非通过synchronized关键字,而是通过java.util.concurrent.locks.ReentrantLock来实现。 ReentrantLock的实现是基于其内部类FairSync...

可以发现,p就是当前node节点对象的前驱节点,而只有当p是头节点时,判断才不会短路,才能去尝试获取同步状态。否则的话,就会走shouldParkAfterFailedAcquire()方法:

通过shouldParkAfterFailedAcquire()方法,会让前驱节点不是头节点的节点进入等待。

这时就会有为什么只有前驱节点是头节点才能尝试获取同步状态的疑问了?

因为只有头节点才是成功获取了同步状态的节点,而当头节点释放了同步状态后,头节点会唤醒它的后继节


深入剖析基于并发 AQS的( 独占锁)重入锁(ReetrantLock)及..._CSDN博客
8-31
深入 理解Java并发之synchronized实现原理 Java并发编程-... AQS工作原理概要 基于ReetrantLock分析 AQS独占模式实现...取锁的 过程中可中断,注意synchronized在 获取锁时是不...
深入浅出 AQS独占锁模式_凌风郎少-CSDN博客
8-28
首先先从整体流程入手,了解下 AQS 独占锁的执行逻辑,然后再一步一步 深入分析源码。 获取锁的 过程: 1. 当线程调用acquire()申请 获取锁资源,如果成功,则进入临界区...
首先会尝试设置状态从小于0变成0。一般可以这样认为,如果head的状态为0,代表head后继线程即将被唤醒,或者已经被唤醒。 如果遇到s == null,说明我们遇到一种中间状态,next指针还没有指好。如果遇到s.waitStatus > 0,说明head后继刚取消了。这两种情况,都需要从队尾的prev往前找。关于prev的有效性,已经在上一篇博客讲过了。 注意循环条件t != null && t != node,它会从队尾一直往前找,直到t是null或t已经到达了node。一般情况下,不会出现t !=
目录
    1.前言
    2.AbstractQueuedSynchronizer介绍
    2.1 <em>AQS</em>是构建同步组件的基础
    2.2 <em>AQS</em>的内部结构(ReentrantLock的语境下)
    
    3 非公平模式加锁流程
    3.1加锁流程真正意义上的入口




逐行分析 AQS源码(1)—— 独占锁获取_weixin_34185560..._CSDN博客
8-31
本篇我们将以ReentrantLock的公平锁为例来详细看看使用 AQS 获取 独占锁的流程。本文中的源码基于JDK1.8 。 系列文章目录Java并发工具类的三板斧在开始看 AQS源码之前,...
深入浅出 AQS独占锁模式 - m0_38053538的博客 - CSDN博客
10-18
首先先从整体流程入手,了解下 AQS 独占锁的执行逻辑,然后再一步一步 深入分析源码。 获取锁的 过程: 当线程调用acquire()申请 获取锁资源,如果成功,则进入临界区。 当...
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值