AQS源码解析之独占锁模式

简介

AbstractQueuedSynchronizer简称AQS,即抽象队列同步器。它是JUC包下的核心组件,是用来构建锁或者其他同步组件的骨架类,减少了各功能组件实现的代码量,也解决了在实现同步器时涉及的大量细节问题,例如等待线程采用FIFO队列操作的顺序。它的主要使用方式是 继承,子类通过继承AQS,实现它的抽象方法来管理同步状态,它分为独占锁和共享锁,独占锁和共享锁的最大不同就是在同一时刻能否有多个线程获取同步状态,通过调用acquireShared方法获取同步状态。很多同步组件都是基于它来实现的,比如ReentrantLock,它是基于AQS的独占锁实现的,它表示每次只能有一个线程持有锁。另外ReentrantReadWriteLock它是基于AQS的共享锁实现的,它允许多个线程同时获取锁,并发的访问资源。AQS是建立在(CompareAndSet,CAS)上的一种FIFO的双向队列,它通过在内部维护着一个volatile修饰的同步状态值state,从而保证线程的安全执行。
AQS对于状态的更改提供了三个方法:

  1. getState():返回同步状态的当前值
    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }
  1. setState():设置当前同步状态
    /**
     * Sets the value of synchronization state.
     * This operation has memory semantics of a {@code volatile} write.
     * @param newState the new state value
     */
    protected final void setState(int newState) {
        state = newState;
    }
  1. compareAndSetState():通过CAS设置当前状态,该方法能够保证状态的原子性。它是通过Unsafe这个类中的native方法来保证的。
    /**
     * Atomically sets synchronization state to the given updated
     * value if the current state value equals the expected value.
     * This operation has memory semantics of a {@code volatile} read
     * and write.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that the actual
     *         value was not equal to the expected value.
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
	// Unsafe#compareAndSwapInt()
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
AQS原理

如果请求的共享资源空闲,那么就把当前请求的线程设置为工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源占用,那么需要一套线程阻塞等待以及唤醒的锁的分配机制。这套机制AQS是用CLH队列锁实现的(由于是 Craig、Landin 和 Hagersten三位大佬的发明,因此命名为CLH锁)。获取不到锁的线程将加入到队列中。AQS内部维护的一个同步队列,获取失败的线程会加入到队列中进行自旋(其实就是for死循环),移除队列条件是前驱节点是头节点并且成功获取到了同步状态,释放同步状态AQS会调用unparkSuccessor方法唤醒后继节点。
在这里插入图片描述

AQS数据结构

AQS队列内部维护的是一个FIFO的双向链表,如下图所示。这种结构的特点是每个数据结构都有两个指针,分别指向直接前驱节点和直接后继节点。这种数据结构优势在于可以从任意的一个节点开始很方便的访问前驱和后继节点。每个Node由线程封装,当竞争失败后会加入到AQS队列中去。
在这里插入图片描述
Node源码如下:

static final class Node {
        // 共享模式下等待的标记
        static final Node SHARED = new Node();
        // 独占模式下等待的标记
        static final Node EXCLUSIVE = null;
 
        // 线程的等待状态 表示线程已经被取消
        static final int CANCELLED =  1;
        // 线程的等待状态 表示后继线程需要被唤醒
        static final int SIGNAL    = -1;
        // 线程的等待状态 表示线程在Condtion上
        static final int CONDITION = -2;
        
        // 表示下一个acquireShared需要无条件的传播
        static final int PROPAGATE = -3;
 
        /**
         *   SIGNAL:     当前节点的后继节点处于等待状态时,如果当前节点的同步状态被释放或者取消,
         *               必须唤起它的后继节点
         *         
         *   CANCELLED:  一个节点由于超时或者中断需要在CLH队列中取消等待状态,被取消的节点不会再次等待
         *               
         *   CONDITION:  当前节点在等待队列中,只有当节点的状态设为0的时候该节点才会被转移到同步队列
         *               
         *   PROPAGATE:  下一次的共享模式同步状态的获取将会无条件的传播
 
         * waitStatus的初始值时0,使用CAS来修改节点的状态
         */
        volatile int waitStatus;
 
        /**
         * 当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候才被分配,
         * 并且只在出队的时候才被取消(为了GC),头节点永远不会被取消,一个节点成为头节点
         * 仅仅是成功获取到锁的结果,一个被取消的线程永远也不会获取到锁,线程只取消自身,
         * 而不涉及其他节点
         */
        volatile Node prev;
 
        /**
         * 当前节点的后继节点,当前线程释放的才被唤起,在入队时分配,在绕过被取消的前驱节点
         * 时调整,在出队列的时候取消(为了GC)
         * 如果一个节点的next为空,我们可以从尾部扫描它的prev,双重检查
         * 被取消节点的next设置为指向节点本身而不是null,为了isOnSyncQueue更容易操作
         */
        volatile Node next;
 
        /**
         * 当前节点的线程,初始化后使用,在使用后失效 
         */
        volatile Thread thread;
 
        /**
         * 链接到下一个节点的等待条件,或特殊的值SHARED,因为条件队列只有在独占模式时才能被访问,
         * 所以我们只需要一个简单的连接队列在等待的时候保存节点,然后把它们转移到队列中重新获取
         * 因为条件只能是独占性的,我们通过使用特殊的值来表示共享模式
         */
        Node nextWaiter;
 
        /**
         * 如果节点处于共享模式下等待直接返回true
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
 
        /**
         * 返回当前节点的前驱节点,如果为空,直接抛出空指针异常
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
 
        Node() {    // 用来建立初始化的head 或 SHARED的标记
        }
 
        Node(Thread thread, Node mode) {     // 指定线程和模式的构造方法
            this.nextWaiter = mode;
            this.thread = thread;
        }
 
        Node(Thread thread, int waitStatus) { // 指定线程和节点状态的构造方法
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

AQS添加节点

AQS将节点加入到同步队列的过程如下图所示:
在这里插入图片描述
加入队列的过程必须是线程安全的,所以AQS提供了一个基于CAS设置尾节点的方法compareAndSetTail,这个也是Unsafe类中的native方法。它需要传入当前线程的认为的尾节点和当前节点,当设置成功后,当前节点和尾节点连接起来,当前节点正式加入到队列中。源码如下:

    /**
     * CAS tail field. Used only by enq.
     */
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
    // Unsafe#compareAndSwapObject()
    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
AQS重要方法

AQS使用了模版方法模式(设计模式的一种),自定义同步器必须重写下面的几个AQS提供的模版方法,否则直接抛出异常。

// 该线程是否处于独占资源。只有用到Condition才需要实现它
isHeldExclusively()
// 独占方式获取资源,成功返回true,失败返回false
tryAcquire(int)
// 独占方式释放资源,成功返回true,失败返回false
tryRelease(int)
// 共享方式获取资源。负数表示失败,0表示成功但是没有剩余可用资源;
// 正数表示成功且有剩余资源
tryAcquireShared(int)
// 共享方式释放资源.成功返回true,失败返回false
tryReleaseShared(int)
AQS独占锁模式

独占锁的获取是通过AQS提供的acquire()。源码如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

可以发现,acquire()获取同步状态成功与否做了2件事情。(1)成功,方法直接返回;(2)失败,会将当前线程加入到同步队列,它是通过调用addWaiter()acquireQueued()方法实现的,源码如下:

    /**
     * 把Node节点添加到同步队列的尾部
     */
    private Node addWaiter(Node mode) {
        // 第一步
        Node node = new Node(Thread.currentThread(), mode);  // 以独占模式把当前线程封装成一个Node节点
        // 尝试快速入队
        Node pred = tail;  // 当前队列的尾节点赋给pred
        // 第二步
        if (pred != null) {  // 先觉条件 尾节点不为空
            node.prev = pred;  // 把pred作为node的前继节点
            if (compareAndSetTail(pred, node)) { //利用CAS把node作为尾节点
                pred.next = node;    // 把node作为pred的后继节点
                return node;       // 直接返回node
            }
        }
        // 第三步
        enq(node);  // 尾节点为空或者利用CAS把node设为尾节点失败
        return node;
    }
    /**
     * 采用自旋的方式把node插入到队列中
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // 如果t为空,说明队列为空,必须初始化
                if (compareAndSetHead(new Node())) // 新建一个节点利用CAS设为头节点,就是这样的形式 head=tail=null
                    tail = head;
            } else {    // 尾节点不为空的情况
                node.prev = t;  // 把t设为node的前驱节点
                if (compareAndSetTail(t, node)) {  // 利用CAS把node节点设为尾节点
                    t.next = node;   // 更改指针  把node作为t的后继节点
                    return t;   // 直接返回t
                }
            }
        }
    }

通过方法会发现它先会把当前线程封装成Node类型,然后判断尾节点是否为空,如果不为空进行CAS操作加入队列;如果为空,那么会调用enq()这个方法,次方法则是通过不断的for循环自旋CAS尾插入节点。
上述就是独占锁获取失败加入队列的过程,那么对于同步队列的节点会做什么事情来保证自己有机会获取独占锁呢?acquireQueued()源码如下:

    /* 
     * 此主要是通过自旋方式获取同步状态
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;  // 默认线程没有被中断过
            for (;;) {
            	// 第一步
                final Node p = node.predecessor();  // 获取该节点的前驱节点p
                // 第二步                
                if (p == head && tryAcquire(arg)) {  // 如果p是头节点并且能获取到同步状态
                    setHead(node);                   // 把当前节点设为头节点
                    p.next = null;                  // 把p的next设为null,便于GC
                    failed = false;                 // 标志--表示成功获取同步状态,默认是true,表示失败
                    return interrupted;             // 返回该线程在获取到同步状态的过程中有没有被中断过
                }
                // 第三步
                if (shouldParkAfterFailedAcquire(p, node) &&   // 用于判断是否挂起当前线程
                    parkAndCheckInterrupt())
                    interrupted = true;      
            }
        } finally {
            if (failed)   // 如果fail为true,直接移除当前节点
                cancelAcquire(node);
        }
    }

从源代码中可以看出这是一个自旋过程for(;;),首先是先获取当前节点的前驱节点,然后判断当前节点能否获取独占锁,如果前驱节点是头节点并且获取同步状态,那么就可以获取到独占锁。shouldParkAfterFailedAcquire()这个方法主要的逻辑是调用compareAndSetWaitStatus(),使用CAS将节点状态由INITIAL设置为SIGNAL。如果失败则会返回false,通过acquireQueued()的自旋转会继续设置,直到设置成功。设置成功后调用parkAndCheckInterrupt()方法,此方法会调用LockSupport.park(this)让该线程阻塞。

AQS独占锁获取流程图

在这里插入图片描述

AQS独占锁释放

独占锁的释放是用release()方法,源码如下:

 /**
 * 以独占模式释放同步状态,当前线程释放同步状态的时候,会唤醒同步队列上的后继节点
 * 释放成功后之后直接返回true
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

如果同步状态释放成功,会执行if语句内的代码,当head不为空并且状态不为0的时候会执行unparkSuccessor()方法,unparkSuccessor()方法会执行LockSupport.unpark()方法。每一次释放锁就会唤醒队列中该节点的后继节点。
参考自 AQS独占式

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值