java 并发基石——AQS

235 阅读23分钟

1.概念

​ AQS全称AbstractQueuedSynchronize(java.util.concurrent.locks),即抽象的队列式的同步器。

​ AQS定义了一套多线程访问共享资源的同步器框架,许多锁和同步器实现都依赖于它,如常用的而ReentrantLock、Semaphore、LockSupport等java.util.concurrent包下大量的锁和同步器。【AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架】

private static final class Sync extends AbstractQueuedSynchronizer {
    *****
}

AQS 定义了两种资源共享方式:

  1. Exclusive独占,只有一个线程能执行,如ReentrantLock
  2. Share共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier

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

ReentrantReadWriteLock【读写锁】

​ 它表示两个锁,一种是读操作相关的锁,称为共享锁;一种是写相关的锁,称为排他锁。

不同的自定义的同步器争用共享资源的方式也不同。

2.AQS内部结构及原理

加锁会导致阻塞、有阻塞就需要排队,实现排队必然需要队列。如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。

image.png

  1. 内置的**CLH(FIFO)队列**的变种来完成资源获取线程的排队工作
  2. 将每条将要去抢占资源的线程封装成一个**Node节点**来实现锁的分配
  3. 有一个int类变量表示持有锁的状态(private volatile int state),通过**CAS原子的修改共享标志位**、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。

CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO

Node

   static final class Node {
        /** 节点在共享模式下等待 */
        static final Node SHARED = new Node();
        /** 节点在独占模式下等待 */
        static final Node EXCLUSIVE = null;

        /**
         * 表示当前结点已取消调度
         * 当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。 
         */
        static final int CANCELLED =  1;
        /** 
         * 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL
         */
        static final int SIGNAL    = -1;
        /** 
         * 表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等			* 待获取同步锁。
         */
        static final int CONDITION = -2;
        /**
         * 共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点
         */
        static final int PROPAGATE = -3;

        /**
         * 节点在队列种的等待状态
         */
        volatile int waitStatus;

        /**
         * 前驱节点
         */
        volatile Node prev;

        /**
         * 后继节点
         */
        volatile Node next;

        /**
         * 线程对象
         */
        volatile Thread thread;

        /**
         *
         */
        Node nextWaiter;
    }

3.成员变量

    /**
    * 头节点
    */
    private transient volatile Node head;

    /**
     * 尾节点
     */
    private transient volatile Node tail;

    /**
     * 标识同步状态
     */
    private volatile int state;

1.state

用于判断共享资源是否正在本占用的标志位,volatile保证线程之间的可见性。

线程获取锁的两种模式:

  1. 独占:一个线程以独占模式获取锁时,其他线程必须等待。仅有一个线程可执行
  2. 共享:一个线程以共享模式获取锁时,其他也想以共享模式获取锁的线程,也能够增强锁,从而一起访问共享资源。可以多个线同时执行。

在共享模式下,可能存在多个线程正在共享资源,所以state需要表示线程占用的个数,所以使用了int类型,而非Boolean类型

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

2.head、tail

头节点、尾节点

一个线程在当前时刻没有获取到共享资源,可以进行排队。 而排队的队列数据结构则是一个FIFO(先进先出的双向链表),head和tail 则表示该链表的头和尾。

4.主要方法

  1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

1.acquire

**获取资源:**此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。这也正是lock()的语义,当然不仅仅只限于lock()。获取到资源后,线程就可以去执行其临界区代码了。

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

​ 该方法用public final 修饰,表示不允许所有的子类进行修改,直接调用父类方法即可。说明该方法一定可以获得锁。

执行过程:

​ tryAcquire(arg)尝试获取锁(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待)

  1. tryAcquire(arg)尝试获取锁成功,acquire()方法直接结束

  2. tryAcquire(arg)尝试获取锁失败,需要执行addWaiter()方法,该线程加入等待队列的尾部,并标记为独占模式。接着执行acquireQueued(Node node, int arg)方法。

    acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。且整个过程忽略中断的影响。

  3. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上

tryAcquire

尝试获取资源

// 参数arg  表示对state的修改值,返回值Boolean类型,表示是否获取到锁。
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}

该方法protected修饰,且仅一行报错代码。需要其子类进行重写。

这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。

该上层调用开放了空间。

AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现

  • 尝试获得锁,获取锁或者未获得锁后,进行相应的业务处理。

若业务一定需要获得锁,则下面的acquire()方法可以保证线程一定可以获得锁。

addWaiter

将当前线程封装成一个Node节点,加入等待队列。放回当前节点。

private Node addWaiter(Node mode) {
    //1. 将当前线程封装成Node节点   mode有两种:EXCLUSIVE(独占)和SHARED(共享)
        Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure(先尝试快速入队,如果失败进行完整入队)
    //2.将新建的节点插入到队尾
    //2.1 获取尾节点
        Node pred = tail;
        if (pred != null) {
            // 2.2 将尾节点作为将新的Node节点 的前驱节点
            node.prev = pred;
            // 2.3 在尾节点不为空的情况下,通过CAS 将当前节点置为尾节点 
            if (compareAndSetTail(pred, node)) {
                // 2.4 将之前的尾节点(倒数第二个)的后继节点置为新的Node节点(2.3CAS为新的尾节点)
                pred.next = node;
                return node;
            }
        }
    // 3. 当前尾节点为空 或者 第一次CAS修改尾节点失败时,将执行下面的完整入队方法。
        enq(node);
        return node;
    }
// 2.3 在尾节点不为空的情况下,通过CAS 将当前节点置为尾节点 
if (compareAndSetTail(pred, node)) {
  // 2.4 将之前的尾节点(倒数第二个)的后继节点置为新的Node节点(2.3CAS为新的尾节点)
  pred.next = node;
  return node;
}

​ compareAndSetTail()方法虽然是一个原子操作,但是整个if代码块并不是一个原子操作。

当2.4正在修改之前尾节点的后继节点时,可能其他线程正在修改尾节点信息。而此时即便尾节点发生了变动,对此操作也不会出现线程安全问题。

完整的入队方法enq()👇👇👇👇👇

private Node enq(final Node node) {
    // 自旋,直到成功加入队尾
    for (;;) {
        Node t = tail;
        // // 队列为空,创建一个空的标志结点作为head结点,并将tail也指向它。
        if (t == null) { // Must initialize
            // 由此可以看出Head节点其实是一个傀儡节点,不包含数据,Head节点并不是当前需要获取锁的节点,
            // 只是一个占位的摆设,第二个节点才是需要获取锁的节点
            // 所以在AQS 中多出出现判断一个节点的前驱节点是否为Head节点的逻辑
            if (compareAndSetHead(new Node()))
                tail = head;
       //正常流程,放入队尾
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

通过自旋,初始化队列,将当前节点插入到队列

acquireQueued

进入等待状态休息,直到其他线程彻底释放资源后唤醒自己,自己再拿到资源

final boolean acquireQueued(final Node node, int arg) {
    // 标记是否成功拿到资源
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 如果前驱是head,即该结点已成老二。(Head节点作为一个傀儡节点,不需要获取资源)那么便有资格去尝试获取资源
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC  【之前的Head节点出队】
                failed = false; // 成功获取资源
                return interrupted; //返回等待过程中是否被中断过
            }
            
            // 当前节点不是头节点 或者 Head节点尝试获取锁失败 
            // shouldParkAfterFailedAcquire() 判断当前节点是否需要挂起,若需要挂起,调用parkAndCheckInterrupt()进行park()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true; // 如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
        }
    } finally {
        if (failed) // 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
            cancelAcquire(node);
    }
}

​ 变量failed初始值为true,只有在return之前改为false,而finally{}代码块中,有个判断failed状态的操作,会执行cancelAcquire()方法。说明acquireQueued()方法在正常执行且返回时,failed为false,只有执行过程抛出异常,才会执行finally代码块中的cancelAcquire()方法。

​ cancelAcquire() 将Node节点的waitStatus状态置为CANCELLED,以及其他的一些清理工作

方法主体,通过自旋操作,如果当前节点的前节点时Head节点,并且当前线程尝试获得锁成功。当第二个节点获取到锁后,会变为头节点(上面已经分析得出头节点只是一个傀儡节点,不用来获取锁)

if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
           interrupted = true;

shouldParkAfterFailedAcquire()是否需要挂起当前线程,通过前置节点的waitStatus

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   int ws = pred.waitStatus;
   // 如果当前节点的前置节点的waitStatus为SIGNAL ,那么前置节点也在等待获取锁
   // 所以当前节点可以直接返回true,进行挂起休息
   if (ws == Node.SIGNAL)
       return true;
   // 如果当前节点的前置节点的waitStatus大于0,说明其状态为CANCELLED 
   // 可以将其从队列中删除,如此循环,直至当前节点的前置节点的waitStatus小于等于0
   // ⚠⚠⚠ 那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被GC回收!
   if (ws > 0) {
       do {
           node.prev = pred = pred.prev;
       } while (pred.waitStatus > 0);
       pred.next = node;
       
   // 如果当前节点的前置节点的waitStatus是其他状态
   // 既然当前节点已经加入,那么其前驱节点就应该做好准备等待锁
   // 所以通过CAS将前驱节点的waitStatus置为SIGNAL 
   // (如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。有可能失败,人家说不定刚刚释放完呢!)
   } else {
       compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
   }
   return false;
}
private final boolean parkAndCheckInterrupt() {
   // 挂起线程
   LockSupport.park(this); //调用park()使线程进入waiting状态
   return Thread.interrupted(); // 如果被唤醒,查看自己是不是被中断的
}

下面的两种情况返回false,如果shouldParkAfterFailedAcquire()方法返回true,代表当前节点需要被挂起。这就保证了只有Head的后继节点一个节点一直进行CAS获取锁的操作,这样就能最大限度的避免无用的自旋消耗CPU

在shouldParkAfterFailedAcquire()方法返回true时,则执行parkAndCheckInterrupt()方法中的挂起操作。

其中LockSupport.park()方法,是通过调用JVM的native方法

interrupted是返回当前线程的中断标识位,并将其复位为false。

acquireQueued方法总结:

  1. 结点进入队尾后,检查状态,找到安全休息点;
  2. 调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
  3. 被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

总结

acquire()方法总结:

  1. 调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
  2. 没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

线程进行了入队等待,那么何时会被唤醒呢?

  • 一个线程使用完共享资源,并且在释放锁时,需要唤醒其他等待锁的线程

2.release

释放锁:此方法是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。这也正是unlock()的语义

public final boolean release(int arg) {
    // 1.尝试释放锁成功
    if (tryRelease(arg)) {
    Node h = head;
        if (h != null && h.waitStatus != 0)
        // 唤醒等待队列中指定的Node节点(正常情况下,Head节点的后继节点)
        unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease

尝试释放锁

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

可以看出tryRelease()方法和tryAcquire() 一样,都被protected修饰,且仅一行报错代码,都需要其子类进行重写。

 跟tryAcquire()一样,这个方法是需要独占模式的自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-=arg),也不需要考虑线程安全的问题。但要注意它的返回值,上面已经提到了,**release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!**所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。

unparkSuccessor

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)
        // 在此,Head节点的使命已经完成,此时Head只是一个占位的傀儡节点,将其waitStatus置为0这个默认值,才不会影响其他函数的判断
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * unpark 的线程保留在后继节点中,通常只是下一个节点。
     * 但如果被取消或明显为空,则从尾部向后遍历以找到实际的未取消后继者。
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从尾节点开始向前搜索,找到除了Head节点外,waitStatus小于0,且最考前的节点
        // ⚠⚠⚠⚠⚠ 从这里可以看出,<=0的结点,都是还有效的结点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 唤醒指定线程
        LockSupport.unpark(s.thread);
}

unparkSuccessor(Node node) 方法 传进来Head节点,该方法是为了唤醒Head节点后面的Node节点,使获取锁。

但如果Head节点的后继节点为空,则需从尾部先前编辑,找到未取消的节点,使其获得锁

为何要倒叙搜索?

  • 如果Head节点的后继节点为null,则不可以正序进行搜索。
  • 还有原因嘛?

最后当release()方法执行完成,线程释放锁之后,之前head节点后的线程会通过字段获得锁,如此轮询

总结

  1. release()方法中的最后逻辑->unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用T来表示吧。
  2. 此时,再和acquireQueued()联系起来,T被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然T已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,T也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后T把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了

3.acquireShared

**获取资源:**此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

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

acquireShared流程:

  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列,直到获取到资源为止才返回。

tryAcquireShared

尝试获取锁(共享模式情况下)

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

tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:

  • 负值代表获取失败;
  • 0代表获取成功,但没有剩余资源;
  • 正数表示获取成功,还有剩余资源,其他线程还可以去获取。

doAcquireShared

**共享模式下,必须获取资源。**此方法用于将当前线程加入等待队列尾部休息,直到其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回

    private void doAcquireShared(int arg) {
        // 以共享模式加入队列尾部
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true; //是否成功标志
        try {
            boolean interrupted = false; //等待过程中是否被中断过的标志
            for (;;) {
                final Node p = node.predecessor(); //前驱
                //如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的
                if (p == head) {  
                    int r = tryAcquireShared(arg); //尝试获取资源
                    if (r >= 0) { //获取资源成功
                        setHeadAndPropagate(node, r); //将head指向自己,还有剩余资源可以再唤醒之后的线程
                        p.next = null; // help GC
                        if (interrupted) //如果等待过程中被打断过,此时将中断补上。
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // shouldParkAfterFailedAcquire() 判断当前节点是否需要挂起,若需要挂起,调用parkAndCheckInterrupt()进行park()
                // 进入waiting状态,等着被unpark()或interrupt()
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

​ 跟独占模式比,还有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。

​ 那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。老大先唤醒老二,老二一看资源不够,他是把资源让给老三呢,还是不让?答案是否定的!老二会继续park()等待其他线程释放资源,也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。

当然,这并不是问题,只是AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below(记录下旧头以供检查)
        setHead(node); //head指向自己
   
    	  //  如果还有剩余量,继续唤醒下一个邻居线程
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式!

总结:

执行过程:

  1. tryAcquireShared()尝试获取资源,成功则直接返回;
  2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。

其实跟acquire()的流程大同小异,只不过多了个**自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)**

4.releaseShared

​ **释放掉资源后,唤醒后继节点。**此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源

   public final boolean releaseShared(int arg) {
       // 尝试释放资源
        if (tryReleaseShared(arg)) { 
            // 唤醒后继结点
            doReleaseShared();
            return true;
        }
        return false;
    }

跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于独占下可重入的考量;而共享模式下的releaseShared()则没有这种要求,共享模式实质就是控制一定量的线程并发执行,那么拥有资源的线程在释放掉部分资源时就可以唤醒后继等待结点。例如,资源总量是13,A(5)和B(7)分别获取到资源并发运行,C(4)来时只剩1个资源就需要等待。A在运行过程中释放掉2个资源量,然后tryReleaseShared(2)返回true唤醒C,C一看只有3个仍不够继续等待;随后B又释放2个,tryReleaseShared(2)返回true唤醒C,C一看有5个够自己用了,然后C就可以跟A和B一起运行。而ReentrantReadWriteLock读锁的tryReleaseShared()只有在完全释放掉资源(state=0)才返回true,所以自定义同步器可以根据需要决定tryReleaseShared()的返回值。

doReleaseShared

唤醒后继节点

 private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h); //唤醒后继节点
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)  // head发生变化,释放资源结束                 // loop if head changed
                break;
        }
    }

总结

独占和共享两种模式下获取-释放资源(acquire-release、acquireShared-releaseShared)的源码,相信大家都有一定认识了。值得注意的是,acquire()和acquireShared()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,相应的源码跟acquire()和acquireShared()差不多.

问题:

1.parkAndCheckInterrupt()方法中为何需要return Thread.interrupted();

传递中断标志

private final boolean parkAndCheckInterrupt() {
   LockSupport.park(this);
   return Thread.interrupted();
}

在LockSupport.park(this);对此线程进行挂起时,在此期间,有可能在AQS以外的其他操作想要中转这个线程, 调用了这个线程的interrupted()方法。(当线程处于等待状态时,调用interrupted()中断会抛出interruptedException)

而加入一个线程时通过wait、sleep,那么再调用interrupted()中断会抛出interruptedException,而使用LockSupport.park(this)进行挂起,即使再调用interrupted()方法,也不会抛出异常,此时如果其他线程在某个地方调用了该线程的interrupted()方法,只会改变这个线程对象内部的一个中断的状态值,所以这里需要一个变量值记录下来。

如果外部调用了该线程的interrupted()方法,那么该线程被唤醒时,即parkAndCheckInterrupt()中的LockSupport.park(this);,需要return Thread.interrupted();

image-20210908002528903

当线程处于等待队列时,无法响应外部的中断请求,只有当这个线程拿到锁之后,然后再进行中断

2.park()

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。

需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。

3.独占和共享selfInterrupt()的位置

共享:补中断的selfInterrupt()放到doAcquireShared()里

独占:放到acquireQueued()之外

其他:

1.设计模式:模板方法

AQS底层使用了模板方法模式

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

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

自定义同步器在实现的时候只需要实现共享资源state的获取和释放方式即可,至于具体线程等待队列的维护,AQS已经在顶层实现好了。

参考链接:

  1. Java并发之AQS详解
  2. Juc24_AQS的概述、体系架构、深入源码解读(非公平)、源码总结
  3. 【Java并发】并发编程的意义是什么?