AQS原理及源码分析

所谓的AQS,就是AbstractQueuedSynchronizer,它提供了一种实现阻塞锁和一系列依赖FIFO队列的同步器框

架。通过state的状态, 来实现acquire(加锁)和release(解锁)。state为0,表示当前没有线程获取到锁,可以

竞争锁,state为1表示已有线程占有了锁。

在阅读AQS源码前,我们了解AQS的类图和里面的几个主要概念以及AQS开始引入JDK的版本号和作者。

 

可以看到AQS是在JDK1.5版本引入的,作者是大名鼎鼎的Doug Lea。

从上面的图中可以看到AQS继承了AbstractOwnableSynchronizer并实现了Servializable接口,其中

AbstractOwnableSynchronizer中主要是设置和获得占有锁的当前线程,AbstractOwnableSynchronizer不是我

们学习的重点,在这里就不做展开了。

重要字段

在AQS中有三个比较重要的字段,它们分别是head、tail、state。它们分别代表的意思是链表头结点、尾结点、锁

状态。

 

并且它们都是volatile关键字修饰的,保证了它们在不同线程之间的可见性。其中head和tail字段的类型为Node,

Node是AQS的一个内部类,其维护了一个双向链表的数据结构,这也就是我们为什么在文章开头说AQS是依赖

FIFO链表的同步器。

Node类中包含了5个重要字段,分别是prev、next、thread、waitStatus、nextWaiter,分别代表的意思是前驱

节点、后继节点、节点持有的线程、节点状态、下一个等待触发的节点。

Node源代码解析如下:

```java
static final class Node
{
    // 共享模式节点
    static final Node SHARED = new Node();

    // 独占模式节点
    static final Node EXCLUSIVE = null;

    // 表示等待线程已放弃获取锁(线程因timeout和interrupt而放弃竞争state)
    static final int CANCELLED = 1;

    // 表示等待线程需要唤醒后继线程(当前节点释放state或者取消后,将通知后续节点竞争state)
    static final int SIGNAL = -1;

    // 表示等待线程正在等待条件触发
    static final int CONDITION = -2;

    // 表示下一个acquireShared应无条件传播
    static final int PROPAGATE = -3;

    //节点状态,参考CANCELLED、SIGNAL、CONDITION、PROPAGATE
    volatile int waitStatus;

    // 前驱节点
    volatile Node prev;

    // 后继节点
    volatile Node next;

    // 节点持有的线程
    volatile Thread thread;

    // 下一个等待条件触发的节点
    Node nextWaiter;

    final boolean isShared()
    {
        return nextWaiter == SHARED;
    }

    /**
         * 返回前驱节点
         *
         * @return the predecessor of this node
         */
    final Node predecessor()
        throws NullPointerException
    {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node()
    { // Used to establish initial head or SHARED marker
    }

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

    Node(Thread thread, int waitStatus)
    { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}
```

从Node类中我们可以看到AQS支持独占锁和共享锁,下面我们就分别分析这两种锁的加锁和解锁过程。

独占锁(加锁过程)

独占锁的加锁是通过acquire方法实现的,我们就以这个方法为入口,来分析它的加锁过程,源码截图如下:

可以看到acquire方法中,先调用tryAcquire方法,tryAcquire返回false,才会执行addWaiter方法,addWaiter的

返回值作为acquireQueued的参数再去执行acquireQueued方法。

那么我们先看tryAcquire方法;

可以看到在AQS中tryAcquire并未实现具体的业务逻辑,而是直接抛出了UnsupportedOperationException异

常,这主要是因为AQS只是提供了获取锁和释放锁的能力,至于我们怎么获取?获取什么样的锁?,是由具体的锁

来实现的,在这里我们就先不过多说明,在以后的 ReentrantLock 中我们再详细介绍。假设我们现在认为

tryAcquire返回的是false,直接进入到acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法中,进入这个方法

后先执行addWaiter方法,并传入Node.EXCLUSIVE参数,这个参数的意思是标识获取锁定类型是独占锁。

接着我们看addWaiter的方法;

在addWaiter中主要有三个操作:

  1. 创建一个持有当前线程的节点,并标志获取锁的类型。

  2. 先判断链表的尾节点是否为空,如果不为空,采用CAS将节点插入到队尾。

  3. 如果第二步中链表的尾节点为空或CAS入队失败(有可能有其它线程也在入队),则通过enq将节点入队。

接下来我们在enq方法;

在enq中主要是通过自旋(死循环)做两件事情:

  1. 初始化队列,并帮我们生成一个头节点(头节点中的thread属性为空)。

  2. 将我们传入的节点入队,并维护好节点之间的prev、next关系。

在这里有一个比较重要也是网上很多博客未提及的地方,就是假如我们需要入队的节点是第一个节点(即队列未初

始化),那么AQS会帮我们生成一个头节点(节点中的thread属性为空),而我们需要入队的节点作为链表的第

二个节点,生成的链表结构入下图:

接着我们看acquireQueued方法;

在acquireQueued也是通过自旋方式去获取锁,当节点获取到锁后,会把节点置为队列的头节点,同时会把节点

中thread属性设为空(详情参考setHead方法),把之前的头结点的next属性设为空,并标记锁获取成功。 在自

旋获取锁时,不是无限自旋,一般会存在两种情况:

  1. 自旋一次就获取到了锁。

  2. 自旋两次就会把线程给park住,直到其他线程唤醒它,再去自旋获取锁。

至于为什么会有两种情况,会在后面分析 ReentrantLock 源码的时候详细说明,主要是因为在自旋的时候调用了

tryAcquire 方法,这个方法和 acquire 方法类似,也是由具体的锁去实现的。

接着我们看下shouldParkAfterFailedAcquire方法;

在shouldParkAfterFailedAcquire方法中主要是对前驱节点的等待状态进行设置和判断,来保证acquireQueued

方法中自旋的次数。

接下来我们再看parkAndCheckInterrupt方法;

这个方法比较简单,主要是park住线程,待线程被唤醒时,返回线程的中断状态。

到此独占锁加锁的过程就介绍完了,下面我们通过一个流程图,来总结一下这个过程

独占锁(解锁过程)

独占锁的解锁是通过release方法实现的,我们也以这个方法为入口,来分析它的加锁过程,源码截图如下:

独占锁的解锁过程比较简单,先调用tryRelease方法解锁,如果解锁成功并且队列中还存在其他节点则调用

unparkSuccessor唤醒头结点的后续节点来获取锁;如果队列中不存在其他节点则直接返回true,表示解锁成功。

我们先看tryRelease方法;

tryRelease和加锁过程中的tryAcquire方法一样,AQS也只是提供了一种解锁能力,具体的解锁逻辑由具体的锁

(子类)去实现。

接着我们看unparkSuccessor方法;

在unparkSuccessor方法中主要有三个操作:

  1. 检查头节点是waitStatus的状态,如果小于0,采用CAS设置为0。

  2. 获取头节点的后续节点,如果后续节点为空或已取消获取锁(waitStatus=1),则从后往前遍历找到离头节点

    最近的且正在等待获取锁的节点(即waitStatus<=0),并将该节点赋值给s变量。

  3. 找到最近等待获取锁的节点后,通过unpark唤醒该节点,该节点被唤醒后,就进入到acquireQueued()的if (p

    == head && tryAcquire(arg))的判断中,此时被唤醒的线程将尝试获取资源。

到此独占锁的加锁和解锁过程就全部分析完了,下面开始我们分析共享锁的加锁和解锁过程了。

共享锁(加锁过程)

共享锁的加锁和解锁过程与独占锁类似,AQS也是提供了两个API方法来实现的,分别是 acquireShared 和

releaseShared方法,下面我们先分析加锁过程(acquireShared方法 ),源码截图如下:

在加锁过程中,AQS也是先通过 trayAcquireShared 尝试加锁,如果加锁成功直接返回,若加锁失败则执行

doAcquireShared ,下面我们先看 trayAcquireShared 方法;

trayAcquireShared也是需要具体的锁去实现业务逻辑,trayAcquireShared 的返回值若大于0表示获取锁成功且

有剩余锁,若等于0表示获取锁成功且没有剩余锁,若小于0表示获取锁失败。

接下来我们再看 doAcquireShared 方法;

doAcquireShared方法和之前独占锁的 acquireQueued 方法很相似,也是将当前获取锁的线程加入到队列中,然

后自旋获取锁,一般也是自旋两次任未获取到锁,则park住线程,等待唤醒。和 acquireQueued 的区别有两点:

  1. 若节点获取到锁且还有剩余锁,会去唤醒后面的节点(通过 setHeadAndPropagate方法)。

  2. 若之前节点被中断过,则会进行自我中断(selfInterrupt)。

下面具体分析下 setHeadAndPropagate 方法,看它是如何唤醒后续节点的

可以看到主要是通过 doReleaseShared 方法去唤醒后面的节点,继续跟踪

至此共享锁的加锁过程就分析完了,共享锁加锁的核心思想就是自己获取到共享锁后发现系统中还有剩余锁,则唤

醒后面的节点去获取锁。

共享锁(解锁过程)

共享锁的解锁是通过 releaseShared 方法实现的,我们就以这个方法为入口进行分析

解锁是先调用 tryReleaseShared 方法尝试去解锁,若解锁成功再调用 doReleaseShared 方法去唤醒后面的线

程。tryReleaseShared 和 trayAcquireShared方法类型,AQS也是只提供了能力,需要具体的锁去实现。而

doReleaseShared方法在加锁过程中已经分析过了,就不再赘述。

至此,共享锁的加锁和解锁过程就分析完了。

总结

在独占锁和共享锁的加锁过程中,是忽略线程中断的,也就是说当线程在等待队列中等待获取锁的时候,若在外部

将线程中断,独占锁和共享锁是没办法中断这个等待过程的。AQS也可以通过

acquireInterruptibly()/acquireSharedInterruptibly()来支持线程在等待过程中响应中断。

AQS中的独占锁意思就是只有一个线程能持有锁,如:ReentrantLock/ReentrantReadWriteLock的写

锁/ThreadPoolExecutor的Worker,而共享锁的意思是可以有多个线程同时持有锁,如ReentrantReadWriteLock

的读锁/Semaphore/CountDownLatch。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值