AQS源码解析---独占锁获取

本文详细介绍了Java并发库AQS(AbstractQueuedSynchronizer)的核心概念,包括synchronizationstate、Node队列和CAS操作。分析了ReentrantLock的内部实现,从tryAcquire、addWaiter到acquireQueued等关键方法,揭示了公平锁的工作流程。同时,探讨了非公平锁的获取策略,展示了AQS如何保证线程安全并实现高效的锁管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一 简介

二 AQS三大核心概念

synchronization state

 Node 队列

CAS

 三 从ReentranLock走进源码

tryAcquire

addWaiter

end

acquireQueued   

shouldParkAfterFailedAcquire

parkAndCheckInterrupt


 

一 简介

    AQS(AbstractQueuedSynchronizer)是java并发工具的基础,底层很多实现都采用了CAS乐观锁的方式,在锁获取冲突时,通过自旋重试来轻量的获取锁(synchronized的偏向锁、轻量级锁和重量级锁也有CAS操作,我理解目的都是尽可能的减少获取锁带来的上下文开销)。

    AQS是一个抽象类,但是没有抽象方法。这样子类可以根据自己的需要实现对象的方法,而不用实现所有的方法,未实现的方法会默认抛出UnsupportedOperationException异常。图1.1列出了AbstractQueuedSynchronizer的相关方法:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

图 1.1

     AQS继承关系如图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 1.2

二 AQS三大核心概念

synchronization state

    synchronization state为同步状态 ,由全局共享,很多操作会依赖于这个状态。state被设置为volatile类型,保证了其可见性。state大于0表示当前已经有线程持有锁,AQS作者在设计的时候考虑到重入的情况,state的具体正整数值表示当前的重入次数。

23e2137521f24df8b7e4a718d6420571.png

 图 2.1

     通过state能够知道锁被线程持有,但是无法知道被哪个线程持有,AQS通过exclusiveOwnerThread这个变量来记录是哪个线程持有锁,exclusiveOwnerThread变量在AbstractQueuedSynchronizer的父类AbstractOwnableSynchronizer中。

160a7ba1256941e29e3c912fff3115db.png

 图 2.2

 Node 队列

     AQS有一个同步队列,这个同步队列里面的结构是一个Node对象,这个队列存储的是等待获取锁的线程。Node类的相关属性如下,直接复制的源码:

 

/** <p>To enqueue into a CLH lock, you atomically splice it in as new
* tail. To dequeue, you just set the head field.
* <pre>
*      +------+  prev +-----+       +-----+
* head |      | <---- |     | <---- |     |  tail
*      +------+       +-----+       +-----+
* </pre>
*/
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;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

       
        volatile int waitStatus;

        /**
         * Link to predecessor node that current node/thread relies on
         * for checking waitStatus. Assigned during enqueuing, and nulled
         * out (for sake of GC) only upon dequeuing.  Also, upon
         * cancellation of a predecessor, we short-circuit while
         * finding a non-cancelled one, which will always exist
         * because the head node is never cancelled: A node becomes
         * head only as a result of successful acquire. A
         * cancelled thread never succeeds in acquiring, and a thread only
         * cancels itself, not any other node.
         */
        volatile Node prev;

        /**
         * Link to the successor node that the current node/thread
         * unparks upon release. Assigned during enqueuing, adjusted
         * when bypassing cancelled predecessors, and nulled out (for
         * sake of GC) when dequeued.  The enq operation does not
         * assign next field of a predecessor until after attachment,
         * so seeing a null next field does not necessarily mean that
         * node is at end of queue. However, if a next field appears
         * to be null, we can scan prev's from the tail to
         * double-check.  The next field of cancelled nodes is set to
         * point to the node itself instead of null, to make life
         * easier for isOnSyncQueue.
         */
        volatile Node next;

        /**
         * The thread that enqueued this node.  Initialized on
         * construction and nulled out after use.
         */
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  Because condition queues are accessed only
         * when holding in exclusive mode, we just need a simple
         * linked queue to hold nodes while they are waiting on
         * conditions. They are then transferred to the queue to
         * re-acquire. And because conditions can only be exclusive,
         * we save a field by using special value to indicate shared
         * mode.
         */
        Node nextWaiter;

     waitStatus:表示节点所处的状态,初始为0,可能的取值有CANCELLED = 1,SIGNAL = -1, CONDITION = -2, PROPAGATE = -3。

    thread:当前Node代表的线程

    pre:node前驱

    next:node后继 

    nextWaiter:独占锁模式下,该值为null

    特别说明下,从源码中官方说明知道,这个同步队列为一个CLH队列。这个队列永远有一个不代表任何线程的dump node(有情况还是会代表线程的,只是此时它的thread为null,后续会讲),处于队首。当有线程没抢到锁被包装成Node节点放入队列时,如果此时队列为空,会先创建一个dump node,再把这个node节点放到dump node的后面。同步队列示意图如图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 2.3

CAS

     JDK作者为了提高效率,在AQS中,很多状态的改变采用了CAS的方式来改变。如下所示:

/**
     * CAS head field. Used only by enq.
*/
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

/**
* CAS tail field. Used only by enq.
*/
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

/**
* CAS waitStatus field of a node.
*/
private static final boolean compareAndSetWaitStatus(Node node,
                                                         int expect,
                                                         int update) {
        return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                        expect, update);
}

/**
* CAS next field of a node.
*/
    private static final boolean compareAndSetNext(Node node,
                                                   Node expect,
                                                   Node update) {
        return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}

 三 从ReentranLock走进源码

    ReentranLock的大体结构如图所示:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.1 ReentranLock大体结构

     ReentranLock有公平锁和非公平锁两种,默认是非公平锁,创建ReentranLock时通过传入ture new ReentrantLock(true) 就能创建公平锁。以公平锁为例,通过一个demo的方式来debug跟踪源码(该demo网上找的一个示例,这里目的就是为了debug看看具体的流程而言)。demo如下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图3.2 demo

tryAcquire

     先分析获取锁的流程,在tryAcquire里面打个断点,看下调用栈,如图3.3

73b80eb5cfe842a993719a0dcc0f7d2c.png

 图 3.3

 先回到上一步的acquire方法简要说一下:

22bc45c050bf4b469a2382bd2246bc0b.png

 图 3.4

 在acquire方法中涉及到了4个方法,分别作用为:

 (1)tryAcquire(arg):获取锁的具体逻辑,获取锁成功返回true,失败为fasle。

   (2)  addWaiter(Node mode): 获取锁失败后,将当前获取锁的线程包装成Node节点放入同步队列中;

 (3)acquireQueued(final Node node, int arg):尝试获取锁或挂起线程,不是都获取锁失败了吗,这个为什么还会尝试获取锁呢,下面会讲

 (4)selfInterrupt:这个方法的作用可以理解为记录中断,在整个抢锁过程中是不会响应中断的,如果发生了中断,怎么办呢?AQS在抢锁过程中会记录是否发生中断,发生了中断,就调用selfInterrupt()自我中断下,有点感觉像是将抢锁中发生的中断推迟到了抢锁结束后。

回到tryAcquire()方法,如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.5

    通过getState()获取当前状态,如果为0表示当前没有线程占有锁,会进入下一步if的判断。这里是公平锁,所以会先调用hasQueuedPredecessors方法来看之前有没有其他线程在等待锁,如果没有,通过CAS获取到锁后就返回ture了。hasQueuedPredecessors方法代码如下:

 public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

    代码逻辑很简单,就是判断我是不是head节点的后继节点,如果是就表明我是第一顺位,如果不是就说明有其他线程排在我前面在等待获取锁。

    这里假设我是第一顺位,就会通过CAS获取锁。CAS执行的操作是修改state的状态,如果不是重入的,这里会将state的状态由0变为1,如果是重入的,会修改为重入的次数。CAS操作必然只有一个线程会最终成功,成功获取到锁的线程会调用setExclusiveOwnerThread方发将当前锁拥有的线程设置为自己。

854fcf7410294119998c419badebfc04.png

 图 3.6

     如果上诉获取的c大于0,说明有线程占有了锁,会走到else if逻辑,在这个逻辑里会先通过

current == getExclusiveOwnerThread()检查是不是就是我自己,如果是就直接进行锁重入,调用setState(next)改变state的值。

117af7ce33cc4141bbc1eb8bae02a69b.png

 图 3.7

     如果也不是重入锁的话,那就说明抢占锁失败,返回false。这里有一点需要说明下,当c=0获取锁的是否是通过CAS操作对state设值来保证线程安全,但当进入重入逻辑给state设置的时候,是直接调用的setState(next),没有用CAS也没有加锁这些,那么这一步操作线程安全吗?答案是安全的,因为进入到重入的逻辑,说明这个锁已经被持有了(我自己),其他线程是无法进入的。

    抢占锁失败后,会进入到

if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt()

中的addWaiter方法。

addWaiter

     代码如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.8

     由于是独占锁,addWaiter传入的是Node.EXCLUSIVE,该值就为null, 从Node的的构造方法可知:

Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
 }

    独占锁模式下的nextWaiter恒为null。在addWaiter的逻辑很简单,就是通过CAS将当前节点设置为同步队列的尾节点。注意,由于这里会有多个线程会执行设置尾节点的操作,因此失败的线程会进入到end(node)逻辑。还有一种情况,当pred为null,说明队列为空的时候,也会走到end(node)逻辑。

end

    队列为空或队列不为空但加入同步队列失败后,进入到 end(node)逻辑。代码如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.9

     如果队列为空,会调用CAS初始化节点,注意这里是通过new Node()创建了头结点,这个头结点不代表任何线程,就是之前说的dump node。在创建好dump node后,再将当前节点链接到到dump node后。

    当队列不为空的时候,会执行CAS操作将当前节点连接到队列尾巴后,注意看一下代码,有个贼神奇的事情。

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

这段代码做了三件事情:

1.设置node的前驱节点为当前同步队列的尾节点;

2.通过CAS操作将当前尾节点设置为node节点;

3.将老的尾节点的next指向node节点。

这三步一看就不是一个原子操作,因此当有大量线程执行到这里的时候,只有一个线程能够完成上诉3个步骤,其他线程执行完成第一步。会出现一个现象,通过下图来说明:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.10

     从图3.10可知,CAS未成功的Node节点是不会和之前的尾节点关联上的,当有大量线程去执行上诉操作的时候就会出现图3.10的样子。

特别说明,在CAS源码中,在遍历同步队列的时候都是从尾节点开始遍历的,这也是由上诉那三部决定的。如果,当执行完第二步完毕,第三步还没执行完毕的时候,即旧的尾节点的next为null, 要是此时恰好有线程从头遍历队列的话,是遍历不到这个新的尾节点的。如果从尾节点开始遍历,这个时候是能够遍历到所有节点的。因为此时新的尾节点的pre已经指向了旧的尾节点。

    这个for(;;)循环的作用就是会将那些加入队列失败的Node节点,一直会循序加入到队列中,直到成功为止,当所有Node节点都入队成功后,循环终止。如图表示不断循环直到入队成功的中间阶段示意图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.11

acquireQueued   

     addWaiter方法结束后,会进入到acquireQueued方法。先提前声明下,这个方法里面有个神奇的出队操作(我理解是出队),就是如果当前入队的是第一顺位的节点,会将该节点设置为新的dump 节点。还有,在这个方法里有可能还会执行一次抢锁操作,当当前节点的前驱节点是HEAD节点,此时这个节点head节点是dump node,不代表任何线程(thread 为null理解更准确)。说明我当前是第一顺位,所以会再次执行获取锁的操作。acquireQueued代码如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.12

     可以看到,当当前节点node的前驱节点p是头节点时(这个p是个dump节点),会在进行一个尝试获取锁的操作。如果获取锁成功,进入setHead看看会发现新的dump节点诞生了:

/**
     * Sets head of queue to be node, thus dequeuing. Called only by
     * acquire methods.  Also nulls out unused fields for sake of GC
     * and to suppress unnecessary signals and traversals.
     *
     * @param node the node
*/
private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
}

    setHead方法将新的头节点设置为当前节点,同时将thread和pre设置为null,这个操作和最开始初始化队列通过new Node创建的dump节点是一样的。因此,以前的dump节点相当于出队了,新的dump节点诞生了。这里可能有人会有疑问,就是这里把thread设置为null,那么怎么知道当前获取锁的线程是哪个呢?别忘了,在获取锁成功是已经设置一个变量exclusiveOwnerThread来记录当前获取锁的线程。这里thread其实已经没用了。新的dump 节点诞生,这个时候的dump 节点可以理解为是当前持有锁的线程节点。

    源码中在将老的dump节点出队时,执行了p.next = null的操作,目的是为了防止内存泄漏,触发gc的时候能够将老的dump node回收掉,源码中也有官方注释,真的处处是细节,太可怕了

a68b83861f6b43fea022a043ddd91b01.png

 图 3.13

 

    这里的setHead也没有通过CAS或者同步处理,因为是在当前节点获取到锁后执行的操作,所以这里一定是线程安全的,不会有其他线程进来。

shouldParkAfterFailedAcquire

     当前节点不是第一顺位(没有机会再次获取锁,直接失败),或是第一顺位但获取锁失败后会进入到该方法,来判断是否需要将线程挂起。代码如下:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.14

     waitStatus终于派上用场了,先简单说下waitStatus = SIGNAL这个状态。SIGNAL不是表示当前节点的状态,它是表示下一个节点的状态,一个节点的waitStatus被设置SIGNAL是由它的下一个节点设置的,作用是当当前节点释放锁后,它通过SIGNAL来唤醒下一个节点。当一个线程挂起前,它会给它的前驱节点的waitStatus被设置SIGNAL。

    这段代码,首先是获取到前驱节点的状态,如果前驱节点状态为SIGNAL。说明当前节点已将让前驱节点在释放锁的时候通知我了,直接返回ture。

    当ws > 0,只有ws = CANCELLED = 1是大于0的,这里只能为CANCELLED态。开头有讲。

    为CANCELLED态说明前驱节点已经由于超时啊,中断啊等原因放弃了等待锁。因为当前节点需要找到能把自己唤醒的节点啊,所以会继续向前找,直到找到需要等待锁的节点,并将当前节点放在该节点的后面。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 3.15

 如果前驱节点的状态既不是SIGNAL也不是CANCELLED,说明这个是把我唤醒的节点,那么给这个节点的waitStatus被设置SIGNAL。

1fcb43cc64864e0b9e2a9990420748e7.png

 图 3.16

     shouldParkAfterFailedAcquire返回fasle的时候,会退到acquireQueued方法,acquireQueued里面是一个for(;;)死循环,当前节点会继续回到循环中尝试获取锁,反正就一直循环,直到获取锁成功,或获取找到能够唤醒我的前驱节点为止。然后进入parkAndCheckInterrupt方法。

parkAndCheckInterrupt

     parkAndCheckInterrupt代码如下:

 /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
*/
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

到这里,通过LockSupport.park将当前线程挂起,当前线程后面的代码不会执行了,除非其他线程unpark了当前线程。

 

AQS独占锁源码简单分析到此结束,这里感谢https://segmentfault.com/u/chiucheng的博客给了我很多启发。

 

四 补充

    补充下非公平锁的获取,其实逻辑很简单,从源码可知,非公平锁一进来就会尝试获取锁,获取不到才会执行acquire 。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAYWRqMjA=,size_20,color_FFFFFF,t_70,g_se,x_16

 图 4.1

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值