队列同步器AQS-AbstractQueuedSynchronizer 原理分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/J080624/article/details/84849315

【1】AQS 详细介绍

① AbstractQueuedSynchronizer类概览

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。

在这里插入图片描述
其是AbstractOwnableSynchronizer的子类(记住这个类,下面会有用):
在这里插入图片描述

AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器。比如我们提到的ReentrantLock,Semaphore(信号),其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AbstractQueuedSynchronizer其子类如下:
在这里插入图片描述
如图所示,常见的同步器如闭锁,可重入锁、可重入读写锁,信号以及线程池等都依赖AQS-实现了其子类作为内部类用来实现其同步属性。

② AbstractQueuedSynchronizer的Javadoc

AQS提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。

AbstractQueuedSynchronizer被设计为大多数类型的同步器的坚实基础,这些同步器依赖于单个原子{ int state}值来表示状态。子类必须定义改变该状态的protected method,以及定义该状态对于获取或释放该对象意味着什么。除此之外,AbstractQueuedSynchronizer类中的其他方法执行所有排队和阻塞机制。

  • 可能需要重新定义的protected method
protected boolean tryAcquire(int arg);
protected boolean tryRelease(int arg);
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
protected boolean isHeldExclusively()

子类可以维护其他状态字段,但是只有使用方法getState、setState和compareAndSetState操纵的原子更新的{int state}值在同步方面被跟踪。AbstractQueuedSynchronizer的子类应该定义为非公共的内部助手类,用于实现其封闭类的同步属性。

  • 如下所示,这三个方法均为final方法,也是状态获取/修改的核心方法:
protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)

如下所示abstract static class ReentrantLock.Sync部分源码:

 abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //获取当前状态
            int c = getState();
            //0 表示解锁状态 >0 表示占用状态,N表示获取了N次,对应需要释放N次。
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
            	//锁计数+acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                  //设置状态值
                setState(nextc);
                return true;
            }
            return false;
        }
        //...
 }

AbstractQueuedSynchronizer没有实现任何同步接口。相反,它定义了诸如{acquireInterruptibly}之类的方法供锁和相关同步器适当地调用来实现它们的公共方法。

AbstractQueuedSynchronizer支持排他性模式(exclusive-default)或共享模式(shared)之一或两者。当以独占模式获取时,其他线程试图获取的不能成功。由多个线程获取的共享模式可能(但不必)成功。AbstractQueuedSynchronizer类不理解这些差异,除非在机械意义上共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也能够获取。在不同模式中等待的线程共享相同的FIFO队列。通常,实现子类只支持这些模式之一,但是两者都可以发挥作用,例如ReadWriteLock。只支持独占模式或只支持共享模式的子类不需要定义支持未使用模式的方法。

AbstractQueuedSynchronizer类定义了一个嵌套的ConditionObject类,该类可以被支持独占(排他)模式的子类用作Condition实现。方法isHeldExclusively表明当前线程是否独占地保持同步,此方法仅在ConditionObject方法内部调用,因此如果不使用Condition,则不需要定义此方法。在当前状态(getState)下调用release方法则完全释放此对象。方法acquire则会被给予当前保存的状态的值,最终将此对象恢复到其先前获取的状态。没有AbstractQueuedSynchronizer的方法就会创建这样的Condition,因此如果不能满足此约束,则不要使用它。ConditionObject的行为取决于同步器实现的语义。

AbstractQueuedSynchronizer为ConditionObject和内部队列提供了 inspection, instrumentation, and monitoring等类似方法。可以使用AbstractQueuedSynchronizer将其同步机制导出到类中。

这个类的序列化只存储底层的维护状态的原子整数,所以反序列化对象具有空的线程队列。需要序列化的典型子类将定义{readObject}方法,该方法在反序列化时将其恢复到已知的初始状态。

要使用这个类作为自定义同步器的基础,可以通过使用protected fina类型三个方法getState、setState或compareAndSetState检查或修改同步状态,根据需要重新定义以下protected方法:

* tryAcquire
* tryRelease
* tryAcquireShared
* tryReleaseShared
* isHeldExclusively

这些方法默认抛出UnsupportedOperationException。这些方法的实现必须是内部线程安全的,并且通常应该很短而不能阻塞。重新定义这些方法是使用AQS此类的唯一支持方式。AQS的所有其他方法都被声明为final,因为它们不能独立地变化。

该类继承自AbstractOwnableSynchronizer的方法对于跟踪拥有独占同步器的线程非常有用。使用它们将允许监视和诊断工具帮助用户确定哪些线程持有锁。

AbstractOwnableSynchronizer类方法如下:
在这里插入图片描述
AbstractOwnableSynchronizer源码如下(对下面分析AQS很有用):

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /** Use serial ID even though all fields transient. */
    private static final long serialVersionUID = 3737899427754241961L;
    
    protected AbstractOwnableSynchronizer() { }

     //独占模式同步器的当前所有者-线程
    private transient Thread exclusiveOwnerThread;

     //设置当前独有访问的线程。如果参数为null,表示没有线程接入。
     //参数并不需要额外强加任何同步或者volatile类型修饰
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

     //返回被setExclusiveOwnerThread方法最近set的线程,如果没有set,返回null。
     //This method does not otherwise impose any synchronization or {@code volatile} field accesses.
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

即使AbstractQueuedSynchronizer类基于一个内部FIFO队列,它也不会自动执行FIFO获取策略。独占模式同步的核心是:

Acquire:
    while (!tryAcquire(arg)) {
       enqueue thread if it is not already queued;
       possibly block current thread;
     }

 Release:
     if (tryRelease(arg))
       unblock the first queued thread

共享模式类似,但可能涉及级联信号。

因为Acquire的检查是在排队之前调用的,所以新的Acquire线程可能在阻塞和排队的其他线程之前闯入。但是,如果需要,定义tryAcquire和tryAcquireShared方法来禁止闯入(通过在内部调用一个或多个检查方法)从而提供一个公平的FIFO获取顺序。特别的,大多数公平同步器定义tryAcquire方法返回false如果hasQueuedPredecessors(专门为公平同步器设计的方法)的方法返回true。其他变化是可能的。

吞吐量和可伸缩性通常对于default barging(也称为greedy、renouncement和convoy-avoidance)策略是最高的。虽然这不能保证公平或无饥饿,但是允许在稍后排队的线程之前对先前排队的线程进行重新扩展,并且每次重新占用都有机会成功对付传入的线程。而且,虽然获取不像通常那样自旋,但它们可以在阻塞之前执行与其他计算散布的{tryAcquire}的多次调用。当独占同步仅被短暂持有时,这就提供了大部分的好处,而当它不存在时,没有大部分的责任。如果需要,可以通过前面的调用来获取带有“快速路径”检查的方法,可能预先检查{hasContended}和{hasQueuedThreads}来对此进行扩展,以仅在同步器可能不被争用的情况下才这样做。

AbstractQueuedSynchronizer类在某种程度上为同步提供了有效和可伸缩的基础。其是通过将其使用范围专门化到同步器 which can rely on {@code int} state, acquire, and release parameters, and an internal FIFO wait queue。当这还不够时,可以使用java.util.concurrent.atomic类、您自己的自定义java.util.Queue类和LockSupport来构建较低级别的同步器。


③ 官方实例一

这里是一个不可重入的互斥锁类(独占模式),它使用值0表示解锁状态,值1表示锁定状态。虽然非可重入锁并不严格要求记录当前所有者线程,但无论如何,这个类这样做是为了便于监视使用。它还支持conditions和暴露一种仪器方法:

public class Mutex implements Lock, java.io.Serializable {

   // Our internal helper class
   private static class Sync extends AbstractQueuedSynchronizer {
     // Reports whether in locked state
     protected boolean isHeldExclusively() {
       return getState() == 1;
     }

     // Acquires the lock if state is zero
     public boolean tryAcquire(int acquires) {
       assert acquires == 1; // Otherwise unused
       if (compareAndSetState(0, 1)) {
       //AQS父类AOS的方法
         setExclusiveOwnerThread(Thread.currentThread());
         return true;
       }
       return false;
     }

     // Releases the lock by setting state to zero
     protected boolean tryRelease(int releases) {
       assert releases == 1; // Otherwise unused
       if (getState() == 0) throw new IllegalMonitorStateException();
       setExclusiveOwnerThread(null);
       setState(0);
       return true;
     }

     // Provides a Condition
     Condition newCondition() { return new ConditionObject(); }

     // Deserializes properly
     private void readObject(ObjectInputStream s)
         throws IOException, ClassNotFoundException {
       s.defaultReadObject();
       setState(0); // reset to unlocked state
     }
   }

   // The sync object does all the hard work. We just forward to it.
   private final Sync sync = new Sync();
   
//lock  unlock的本质是使用继承自AQS的子类的acquire和release方法
   public void lock()                { sync.acquire(1); }
   public boolean tryLock()          { return sync.tryAcquire(1); }
   public void unlock()              { sync.release(1); }
   public Condition newCondition()   { return sync.newCondition(); }
   public boolean isLocked()         { return sync.isHeldExclusively(); }
   public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
   public void lockInterruptibly() throws InterruptedException {
     sync.acquireInterruptibly(1);
   }
   public boolean tryLock(long timeout, TimeUnit unit)
       throws InterruptedException {
     return sync.tryAcquireNanos(1, unit.toNanos(timeout));
   }
 }

实例一测试:

import java.util.concurrent.CyclicBarrier;
public class TestMutex {
//这里使用栅栏,可以reset count.
    private static CyclicBarrier barrier = new CyclicBarrier(31);
    private static int a = 0;
    private static  Mutex mutex = new Mutex();

    public static void main(String []args) throws Exception {
        //说明:我们启用30个线程,每个线程对i自加10000次,同步正常的话,最终结果应为300000;
        //未加锁前
        for(int i=0;i<30;i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<10000;i++){
                        increment1();//没有同步措施的a++;
                    }
                    try {
                        barrier.await();//等30个线程累加完毕
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
        barrier.await();
        System.out.println("加锁前,a="+a);
        //加锁后
        barrier.reset();//重置CyclicBarrier
        a=0;
        for(int i=0;i<30;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int i=0;i<10000;i++){
                        increment2();//a++采用Mutex进行同步处理
                    }
                    try {
                        barrier.await();//等30个线程累加完毕
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        barrier.await();
        System.out.println("加锁后,a="+a);
    }
    /**
     * 没有同步措施的a++
     * @return
     */
    public static void increment1(){
        a++;
    }
    /**
     * 使用自定义的Mutex进行同步处理的a++
     */
    public static void increment2(){
        mutex.lock();
        a++;
        mutex.unlock();
    }
}

测试结果:

加锁前,a=204889
加锁后,a=300000

④ 官方实例二

这里是一个类似于java.util.concurrent.CountDownLatch的锁存器类,只是它只需要单个signal来触发。因为latch是非排他性的,所以它使用shared模式的acquire and release methods。

public class BooleanLatch {

	private static class Sync extends AbstractQueuedSynchronizer {
	      boolean isSignalled() { return getState() != 0; }
	 
	      protected int tryAcquireShared(int ignore) {
	        return isSignalled() ? 1 : -1;
	      }
	 
	      protected boolean tryReleaseShared(int ignore) {
	        setState(1);
	        return true;
	      }
      }
	 
	    private final Sync sync = new Sync();
	    public boolean isSignalled() { return sync.isSignalled(); }
	    public void signal()         { sync.releaseShared(1); }
	    public void await() throws InterruptedException {
	      sync.acquireSharedInterruptibly(1);
          }
}

⑤ AQS核心思想

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

AQS(AbstractQueuedSynchronizer)原理图:
在这里插入图片描述
其实就是个双端双向链表。当线程获取资源失败(比如tryAcquire时试图设置state状态失败),会被构造成一个结点加入CLH队列中,同时当前线程会被阻塞在队列中(通过LockSupport.park实现,其实是等待态)。当持有同步状态的线程释放同步状态时,会唤醒后继结点,然后此结点线程继续加入到对同步状态的争夺中。

AQS使用一个int成员变量来表示同步状态(state),通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

private volatile int state;//共享变量,使用volatile修饰保证线程可见性

状态信息通过AQS 类的procted final类型的getState,setState,compareAndSetState方法进行操作:

//返回同步状态的当前值
protected final int getState() {  
        return state;
}
 // 设置同步状态的值
protected final void setState(int newState) { 
        state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

⑥ AQS 对资源的共享方式

AQS定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock。

ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读,但是只允许单个线程写。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。


⑦ AQS底层的模板方法模式

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  • 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  • 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。

模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票buyTicket()->安检securityCheck()->乘坐某某工具回家ride()->到达目的地arrive()。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了ride()方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 ride()方法。

AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:

isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;
//正数表示成功,且有剩余资源。

tryReleaseShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。

默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。除了构造方法,AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。


【2】AQS中的Node

① Javadoc

Node结点类是AbstractQueuedSynchronizer中的一个静态内部类,是等待队列中的结点类。这个等待队列是一个"CLH"锁队列的变体。CLH通常用于自旋锁。相反,我们使用它们来阻塞同步器,但是使用相同的基本策略,在其节点的前辈中保存关于线程的一些控制信息。每个节点中的“status”字段跟踪线程是否应该阻塞。一个节点在它的前辈释放时被唤醒。队列的每个节点另外充当保存单个等待线程的特定通知样式监视器。但是,状态字段不控制线程是否被授予锁等。如果线程在队列中是第一个,线程可能会尝试获取资源。但是线程中的第一个队列获取资源并不能保证成功,它只是被赋予竞争权。故而它可能重新等待。

要排队进入一个CH锁,你需要原子地拼接成新的尾巴。要出队,你只要设置头部。

     +------+  prev +-----+       +-----+
     head |      | <---- |     | <---- |     |  tail
     +------+       +-----+       +-----+

插入CLH队列只需要在“tail”上进行单个原子操作,因此存在从未排队到排队的简单原子分界点。类似地,出队列只涉及更新“头部”。然而,节点需要做更多的工作来确定谁是继任者,部分原因是为了处理由于超时和中断而导致的取消(Cancellation)。

“prev”链接(不用于原始的CLH锁)主要用于处理取消(Cancellation)。如果一个节点被取消,它的继任者(通常)被重新绑定到未取消的前辈结点。

"next"链接被用来实现阻塞机制。每个节点的线程id保存在自己的节点中,因此"前辈结点"通过遍历下一个链接(以确定它是哪个线程)来向下一个节点发出唤醒信号。确定继任者必须避免与新排队的节点竞争,以设置其前任的“next”元素。当一个结点的后续结点为空时,可以通过从原子更新的“tail”向后检查来解决这个问题(另外,"next"链接是一个优化,因此我们通常可能不需要向后扫描。)。

Cancellation对基本算法引入了一些稳健性。由于我们必须轮询其他节点的cancellation,因此我们可能无法注意到取消的节点是在我们前面还是后面。这个问题的解决办法是当取消时唤醒继任者(always unparking successors upon cancellation),使他们能够在新的前任上保持稳定,除非我们能够确定一位未被Cancell的前任(谁将承担这一责任)。

CLH队列需要一个虚拟头节点来启动。但是我们不会使用构造器创建,因为如果没有竞争,这将是徒劳的。相反,队列的节点被构造,队列的头和尾部指针在第一次竞争时被设置(即head 和tail是懒初始化的,并不在Node构造的时候就初始化CLH队列的head 和 tail)。

等待Condition的线程使用相同的节点,但使用附加的链接。Condition只需要在简单的(非并发的)链接队列中链接节点,因为它们只在独占时访问。当等待时,结点被插入一个Condition 队列。当唤醒时,结点被传送到主队列。"status"字段的特殊值将标记结点在哪个队列上。


② Node源码

源码如下:

static final class Node {
        //标记一个正在共享模式中等待的节点
        static final Node SHARED = new Node();

        //标记一个正在独占模式中等待的节点
        static final Node EXCLUSIVE = null;

	//waitStatus 值去声明线程被cancelle
        static final int CANCELLED =  1;
        
	//waitStatus 值 去声明继任者线程需要unparking 
        static final int SIGNAL    = -1;
        
     //waitStatus 值 去声明线程等待一个condition 
        static final int CONDITION = -2;
      
         //waitStatus  值声明下一个acquireShared 应该无条件传播
        static final int PROPAGATE = -3;

/**
* waitStatus取值的几种情况:
* 
*SIGNAL:     
当前结点的继任者是阻塞的(通过park方法),所以在当前结点被释放或者取消时必须unpark它的继任者。
为了避免竞争,获取方法必须首先指示它们需要一个signal,然后重试原子获取,然后在失败时阻塞。
* 
*CANCELLED:  
这个结点由于超时或者中断被取消。节点永远离不开这个状态。
特别是,具有取消节点的线程不会再次阻塞。
* 
*CONDITION:  
此节点当前处于条件队列中。
在传输之前,它不会被用作同步队列节点,
此时状态将被设置为0(这里使用这个值与字段的其他用途无关,但是简化了技术)。
* 
* PROPAGATE:  releaseShared 应该被传播给其他结点。 
* 这在doReleaseShared中设置(仅针对头节点)以确保传播继续,即使其他操作已经介入。
* 
 0:   其他情况,一般为结点初始化值
*
值用数字来表示以简化使用。非负值意味着节点不需要signal。
所以,大多数代码不需要检查特定的值,只是为了签名

对于正常同步结点该字段被初始化为0,对于condition 结点则为CONDITION。
使用CAS算法修改(or when possible, unconditional volatile writes)

        volatile int waitStatus;


         //链接到当前节点/线程依赖于检查waitStatus的前驱节点.
         //入队时分配,出队时置null(为了GC)。
         //此外,在取消前置节点时,我们在找到未取消的节点时短路,这总是存在的,因为head节点从未被取消:节点仅由于成功acquire而变为前置节点。
         //被取消的线程永远不会成功acquiring,并且线程只取消自身,而不是任何其他节点。
        volatile Node prev;

         //链接到当前节点/线程在释放时唤醒的后继节点。
         //在排队期间分配,在绕过取消的前辈结点时进行调整,在退出队列时取消(为了GC)。
         //入队操作直到附加之后才分配前一个结点的"next"字段,所以看到null的"next"字段并不一定意味着节点在队列末尾。
         //但是,如果"next"字段看起来是空的,我们可以从尾部扫描"prev's"来进行双重检查。
         //取消节点的"next"字段被设置为指向节点本身,而不是null,以使isOnSyncQueue更容易操作。
        volatile Node next;

        //排队节点的线程。构造时初始化,out后失效。
        volatile Thread thread;

    /*     链接到下个等待状态的结点,或着特殊值SHARED。
         因为条件队列只有在独占模式中保持时才会被访问,所以我们只需要一个简单的链接队列来在节点等待Condition时保持节点。
         然后,他们被转移到队列重新获取。
         因为Condition只能是排他的,所以我们使用特殊值来指示共享模式。*/
        Node nextWaiter;

        //如果结点在共享模式中等待,返回true
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //返回"前辈"结点,如果为null则抛出NullPointerException 。
      //  可以省去NULL检查,但存在以帮助VM
        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;
        }
    }

总结如下:

AQS采用的是CLH队列,CLH队列是由一个一个结点构成的,结点中有一个状态位,这个状态位与线程状态密切相关,这个状态位(waitStatus)是一个32位的整型常量,它的取值如下:

static final int CANCELLED =  1;
static final int SIGNAL    = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
  • CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;
  • SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;
  • CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;
  • PROPAGATE:使用在共享模式头结点有可能处于这种状态,表示锁的下一次获取可以无条件传播;
  • 0:None of the above,新结点会处于这种状态。

【3】AQS中独占式获取和释放源码分析

① 获取

AQS中比较重要的两个操作是获取和释放,以下是各种获取操作:

public final void acquire(int arg);
final boolean acquireQueued(final Node node, int arg)
public final void acquireInterruptibly(int arg);
public final void acquireShared(int arg);
public final void acquireSharedInterruptibly(int arg);
protected boolean tryAcquire(int arg); 
protected int tryAcquireShared(int arg);
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException;
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException;

获取操作的流程图如下:
在这里插入图片描述

方法如下:

//独占模式中获取锁的方法,排队状态时可能多次阻塞和非阻塞。通常用来实现lock方法
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

如果尝试获取锁成功整个获取操作就结束(tryAcquire方法返回true),否则获取锁失败。 尝试获取锁是通过方法tryAcquire来实现的,AQS中并没有该方法的具体实现,只是简单地抛出一个不支持操作异常。

如果获取锁失败,那么就创建一个代表当前线程的结点加入到等待队列的尾部,是通过addWaiter方法实现的,来看该方法的具体实现:

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        //判断pred是否为null
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //这里走入队操作
        enq(node);
        return node;

该方法创建了一个独占式结点,然后判断队列中是否有元素,如果有(pred !=null)就设置当前结点为队尾结点,返回。

如果没有元素(pred==null),表示队列为空,走的是入队操作:

private Node enq(final Node node) {
	//死循环
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
            // 还记得head 和tail 结点都是懒初始化的吧
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	// node的prev设置为t
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                	//t.next 设置为node  双向链表常用操作
                    t.next = node;
                    return t;
                }
            }
        }
    }

enq方法采用的是变种CLH算法,先看尾结点是否为空,如果为空就创建一个傀儡结点,头尾指针都指向这个傀儡结点,这一步只会在队列初始化时会执行。enq内部是个死循环,通过CAS设置尾结点,不成功就一直重试。很经典的CAS自旋的用法。

如果尾结点非空,就采用CAS操作将当前结点插入到尾结点后面,如果在插入的时候尾结点有变化,就将尾结点向后移动直到移动到最后一个结点为止,然后再把当前结点插入到尾结点后面,尾指针指向当前结点,入队成功。

最后,看下acquireQueued方法:

//Acquires in exclusive uninterruptible mode for thread already in
// queue. Used by condition wait methods as well as acquire.
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())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued内部也是一个死循环,只有前驱结点是头结点的结点,也就是当前结点为老二结点,才有机会去tryAcquire。若tryAcquire成功,表示获取同步状态成功,就将此结点设置为头结点;若是非老二结点,或者tryAcquire失败,则进入shouldParkAfterFailedAcquire去判断判断当前线程是否应该阻塞,若可以,调用parkAndCheckInterrupt阻塞当前线程,直到被中断或者被前驱结点唤醒。若还不能休息,继续循环。

其中 setHead(node)源码如下:

 /**
     * 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;
    }

将新加入的结点放入队列之后,这个结点有两种状态,要么获取锁,要么就挂起,如果这个结点不是头结点,就看看这个结点是否应该挂起,如果应该挂起,就挂起当前结点,是否应该挂起是通过shouldParkAfterFailedAcquire方法来判断的:

/**检查并更新一个acquire失败的结点的状态。返回线程是否应该被阻塞。
在所有的acquire的循环中是主要的singnal控制。
Requires that pred == node.prev.
 * @param pred node's predecessor holding status
* @param node the node 
*/
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)// -1
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {// 1 cancelled
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

该方法首先检查前趋结点的waitStatus位,如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了。

如果前趋结点是一个被取消的结点怎么办呢?那么就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止,将找到的这个结点作为它的前趋结点,将找到的这个结点的waitStatus位设置为SIGNAL,返回false表示线程不应该被挂起。

如果前趋结点不是一个被取消的结点的话,就走 compareAndSetWaitStatus(pred, ws, Node.SIGNAL);方法将值设置为 Node.SIGNAL。

上面谈的不是头结点的情况决定是否应该挂起,是头结点的情况呢?

是头结点的情况,当前线程就调用tryAcquire尝试获取锁。如果获取成功就将头结点设置为当前结点,返回。如果获取失败就循环尝试获取锁,直到获取成功为止。

整个acquire过程就分析完了。


② 释放

释放操作有以下方法:

public final boolean release(int arg); 
public final boolean releaseShared(int arg)
protected boolean tryRelease(int arg); 
protected boolean tryReleaseShared(int arg);

在这里插入图片描述

//Releases in exclusive mode.  Implemented by unblocking one or
//more threads if {@link #tryRelease} returns true.
//This method can be used to implement method {@link Lock#unlock}.
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

1.release过程比acquire要简单,首先调用tryRelease释放锁,如果释放失败,直接返回;

2.释放锁成功后需要唤醒继任结点,是通过方法unparkSuccessor实现的:

//Wakes up node's successor, if one exists
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

1、node参数传进来的是头结点,首先检查头结点的waitStatus位,如果为负,表示头结点还需要通知后继结点,这里不需要头结点去通知后继,因此将该该标志位清0。

2、然后查看头结点的下一个结点,如果下一个结点不为空且它的waitStatus<=0,表示后继结点没有被取消,是一个可以唤醒的结点,于是唤醒后继结点返回。如果后继结点为空或者被取消了怎么办?寻找下一个可唤醒的结点,然后唤醒它返回。

需要注意的是这里并没有从头向尾寻找,而是相反的方向寻找,为什么呢?

因为在CLH队列中的结点随时有可能被中断,被中断的结点的waitStatus设置为CANCEL,而且它会被踢出CLH队列,如何个踢出法,就是它的前趋结点的next并不会指向它,而是指向下一个非CANCEL的结点,而它自己的next指针指向它自己。一旦这种情况发生,如何从头向尾方向寻找继任结点会出现问题(因为一个CANCEL结点的next为自己,那么就找不到正确的继任接点。)。

有的人又会问了,CANCEL结点的next指针为什么要指向它自己,为什么不指向真正的next结点?为什么不为NULL?

第一个问题的答案是这种被CANCEL的结点最终会被GC回收,如果指向next结点,GC无法回收。

对于第二个问题的回答,上述Javadoc中有这么一句话: The next field of cancelled nodes is set to point to the node itself instead of null, to make life easier for isOnSyncQueue.大至意思是为了使isOnSyncQueue方法更新简单。isOnSyncQueue方法判断一个结点是否在同步队列,实现如下:

 final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        return findNodeFromTail(node);
    }

如果一个结点的next不为空,那么它在同步队列中;如果CANCEL结点的后继为空那么CANCEL结点不在同步队列中,这与事实相矛盾。

因此将CANCEL结点的后继指向它自己是合理的选择。


【4】AQS共享式获取和释放源码分析

共享式:共享式地获取同步状态。对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,尝试获取同步状态的方法tryAcquire返回值为boolean。

对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在。尝试获取同步状态的方法tryAcquireShared返回值为int。

 protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
 }

返回值情况如下:
 
1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;

2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;

3.当返回值小于0时,表示获取同步状态失败。

获取同步状态–acquireShared:

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
 //返回值小于0,获取同步状态失败,排队去;
 //获取同步状态成功,直接返回去干自己的事儿。
            doAcquireShared(arg);
}

doAcquireShared方法源码如下:

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        //构造一个共享结点,添加到同步队列尾部。
        //若队列初始为空,先添加一个无意义的傀儡结点,再将新节点添加到队列尾部。
        
        boolean failed = true;//是否获取成功
        try {
            boolean interrupted = false;//线程parking过程中是否被中断过
            for (;;) {//死循环
                final Node p = node.predecessor();//找到前驱结点
                if (p == head) {
                //头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态
                    int r = tryAcquireShared(arg);//尝试获取同步装填
                    //r>=0,获取成功,r 值表示剩余可用同步状态
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
//获取成功就将当前结点设置为头结点,若还有可用资源,
//传播下去,也就是继续唤醒后继结点
                        p.next = null; // 方便GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //是否能安心进入parking状态
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//阻塞线程
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

大体逻辑与独占式的acquireQueued差距不大,只不过由于是共享式,会有多个线程同时获取到线程,也可能同时释放线程,空出很多同步状态,所以当排队中的老二获取到同步状态,如果还有可用资源,会继续传播下去。

setHeadAndPropagate方法源码如下:

//Sets head of queue, and checks if successor may be waiting
// * in shared mode, if so propagating if either propagate > 0 or
//* PROPAGATE status was set.
 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
         /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        if (propagate > 0 || h == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

释放同步状态–releaseShared

 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();//释放同步状态
            return true;
        }
        return false;
    }

doReleaseShared

 private void doReleaseShared() {
        for (;;) {
 //死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;          
                    unparkSuccessor(h);//唤醒后继结点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)              
                break;
        }
    }

需要注意的是,共享模式,释放同步状态也是多线程的,此处采用了CAS自旋来保证。

参考博文:
https://blog.csdn.net/aesop_wubo/article/details/7555956
https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html

猜你喜欢

转载自blog.csdn.net/J080624/article/details/84849315