多线程-AbstractQueueSynchronizer源码详解 (AQS独占锁同步的实现)

版权声明:中华人民共和国持有版权 https://blog.csdn.net/Fly_Fly_Zhang/article/details/89438543

AbstractQueueSynchronizer(AQS)源码分析:基于FIFO(先进先出)队列,用于构建锁或者其它相关同步装置的基础框架。

AQS的功能可以分为两大类:独占功能和共享功能

它的所有子类中,要么实现并使用了它独占功能的API,要么使用了共享锁的功能,而不会同时使用两套API,即便是最有名的子类ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁来实现的。
ReentranctLock类主要靠两个内部类实现(FairSync/NonfairSync),这两个类都继承了AQS。 ReentrantLock是AQS独占功能的实现。

AQS原理:

  • AQS使用的是一个int类型的成员变量state来表示当前的同步状态,当state>0时表示已经获取到锁,当state==0时表示释放了锁。对state操作的几个方法都是原子性的。
  • AQS通过内置的同步队列来完成线程的排队工作,如果当前线程获取锁失败,会将当前线程以及等待状态信息构造成一个Node结点加入同步队列中。当同步队列释放时,则会把节点中的线程唤醒,使其获得锁。

AQS的链表队列结构(Node):AQS内部维护了一个以Node为节点实现的链表的队列。

  • 注意:在独占模式下,next和nextWaiter维护的是两个不同的链表。 next维护的是同步队列链表,而nextWaiter维护的是相同condition条件下的等待队列链表。
  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; 
		//后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,
		//将唤醒后继节点。当前节点的后继节点已经(或即将)被阻塞(通过park,取消线程许可,使其阻塞),所以当当前释放或者被取消的时候,一定要unpark(使当前线程获取许可)它的后继节点。
 
        static final int CONDITION = -2;
		//当前节点在等待condition,也就是在condition(条件)的等待队列中,
		//当其它线程对Condition调用signal()方法后,
		//该节点将从等待队列转移到同步队列中(waitStatus值变为0),加入对同步队列锁的获取中。
   
        static final int PROPAGATE = -3;
		//表示当前场景下后续的acquireShared能够得以执行。 共享模式下的锁释放操作应该被传播到其它节点。该状态值在doReleaseShared方法中被设置。

        volatile int waitStatus; //同步状态
         //上面几个常量表示线程的几个状态, 当waitStatus==0,表示当前节点在Condition的锁池中等待获取同步状态
         //该状态值为了简便使用,所以使用了数值类型。
         //非负数值意味着该节点不需要被唤醒(1表示取消线程,0表示正在获取锁)。
         //所以大多数代码并不需要检查该状态的确定值,只需要根据正负即可。
         //对于一个正常的Node,它的初始值为0 
         //对于一个condition队列中的Node,他的初始值为Condition。(如果修改这个值,可以通过AQS提供的CAS修改)
         
         
   ---------------------------------------------------
        volatile Node prev; 
        //前驱结点。当前结点依赖前前驱结点来检查waitStatus,
        //前驱节点是在当前队列入队时被设置的。
        //为了提高GC效率,当前节点在出队时,会把前驱结点设置为null。
        //而且在取消前驱结点中,则会循环一直找到一个非取消的节点,
        //因为头节点是永远不会被取消的,所以一定能找到。
        volatile Node next;
        //后继节点,在当前节点释放时唤醒后继节点。该后继节点也是在入队时被分配的。
        //当前节点被取消时,会重新调整链表节点指向关系。如:节点入队操作完成前,
        //入队操作的并未设置前驱结点的后继结点。所以会看到前驱结点的后继结点为null,
        //但是这不意味着前驱结点就是队列的尾结点,如果后继结点为null,我们可以通过尾结点
        //向前扫描来做双重检测。一个被取消的结点的后继设置为自身(node.next=node)。
        //这样设置会帮助isOnSyncQueue的执行效率。(该方法使用 if(node.next!=node))
        volatile Thread thread; //当前结点的线程。

        Node nextWaiter;
        //代表Condition等待队列链表的后继结点或者代表共享模式的结点SHARED。
        // condition条件队列:因为condition队列只能在独占模式下被访问,
        //我们只需要简单的使用链表队列来链接正在等待条件的结点。在然后他们会被转移到
        //同步队列(AQS队列)再次重新获取。
        //由于条件队列只能在独占模式下使用,所以我们要表示共享模式的结点的话,
        //只要使用特殊值SHATED来表明即可。


         //判断是不是共享线程,
        final boolean isShared() { 
            return nextWaiter == SHARED; 
        }

       
        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;
        }
		//用于Condition条件等待队列构造结点
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

AQS队列内部变量:

在初始化的AQS队列中,head和tail默认为null。

    private transient volatile Node head; 
     //等待队列的head节点,只有当调用的setHead方法的时候才进行加载,属于懒加载。
     //head结点表示正在运行的线程
  
    private transient volatile Node tail; //等待状态的尾节点
  
    private volatile int state; //AQS队列的状态,<0表示溢出 =0表示锁空闲 >0表示锁重入次数。

AQS队列数据结构示例:

在这里插入图片描述

AQS源码解读:

独占式获取同步方法分析:
获取同步状态流程图:

在这里插入图片描述

获取同步状态步骤:
  • 子类Lock调用acquire()独占式地获取锁
  • acquire首先调用tryAcquire(子类实现)尝试性获取锁(一次尝试获取),如果未成功将其加入同步队列尾部;
  • addWaiter()方法将线程包装成Node结点添加到同步队列尾部(先尝试进行一次队列中有结点的添加),不成功调用enq()死循环添加到队列中(如果没有头结点,操作两次,一次new 一个空Node,第二次将当前线程添加到同步队列中) 这样设计的原因是:高并发条件下,大部分都是有头结点的添加,这样设计使得程序效率提高;
  • 在将同步节点添加进同步队列后,调用acquireQueued()方法进入一个自旋(并不是真的自旋)的过程,将重复进行下面操作:
  • 先判断当前节点的前驱是不是头节点,如果是则尝试获得锁(二次尝试获取);
    在这里插入图片描述
  • 如果不是则进入shouldParkAfterFailedAcquire():此方法判断前驱结点的waitStatus(同步状态),如果是SIGNAL = -1说明前驱结点会唤醒当前结点,那么直接将当前结点线程挂起;如果 CANCELLED = 1 >0说明前驱结点要么超时,要么被中断,需要退出同步队列;如果是其它<=0的状态,将其设置为SIGNAL等待下次循环挂起线程。
  • 如果当前结点前驱同步状态 waitStatus是SIGNAL,调用parkAndCheckInterrupt()挂起该线程;
注意:为何JDK原理的死循环都是for(;;)而不是while(1),因为while(1)编译之后是mov eax,1 test eax,eax je foo+23h jmp foo+18h,for(;;)编译之后是jmp foo+23h,可以看出for(;;)指令少,不占用寄存器,没有判断跳转,效率更高 ;
同步状态相关源码:
  • 注意: 源码注释里面同步和等待队列是一个意思,队列其实就是一个锁池,所有线程在里面争抢锁,得到锁对象的执行权。
   /*
    * 该方法首先会调用子类重写的tryAcquire方法来获取同步状态,当获取失败后,
    * 会调用addWaiter来构造同步结点,并将结点加入到同步队列的队尾
    * 最后调用acquireQueue方法自旋获取同步状态。
    */

   //以独占模式获取对象,忽略中断,也就是不响应中断
    public final void acquire(int arg) {  //此方法一般由子类调用
        if (!tryAcquire(arg) &&  //一般子类会重写此方法 当前线程没有获取锁,
        //没有获得锁才会进入&&后半部分
            acquireQueued( //自旋获取锁的过程,
            addWaiter(Node.EXCLUSIVE), arg)) 
            //将未获取锁的线程加入到等待队列,Node.EXCLUSIVE用来标识此结点为独占模式结点
            selfInterrupt(); //触发中断
    }
     //以独占模式获取对象,如果被中断则中止
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
     //当前线程在超时时间内没有获取到同步状态,那么返回false 获取到返回true
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }
    
    // 将当前线程添加到同步队列中
    /*
     * 首先构造一个独占式Node,pred!=null判断同步是否为空,
     * 如果不为空,则通过compareAndSetTail将节点线程安全的添加到队尾
     * 如果pred==null或者CAS添加失败,则调用enq方法通过死循环来添加节点
     */
    private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); //构建独占式Node
    Node pred = tail; //尾结点
    if (pred != null) { //如果不为空,则同步队列中存在节点
        node.prev = pred;//当前节点连接tail,线程私有变量,无法被修改,因此不需要CAS
        if (compareAndSetTail(pred, node)) { //CAS更新尾节点
            pred.next = node; //tail连接当前节点
            return node;
        }
    }
    enq(node);  //队列中无元素或者CAS操作设置尾节点不成功
    return node;
   }
   
   //死循环添加节点
   /*
    * 先判断队列是否为空,如果为空则构造一个空节点,并将队列的首节点和尾节点设置为当前节点
    * 这样再一次进入循环后,将直接进入else语句通过CAS设置尾节点。
    */
   private Node enq(final Node node) {
    for (;;) {
        Node t = tail; //尾节点
        if (t == null) { //尾节点为空,证明head==null,说明同步队列还未初始化
            if (compareAndSetHead(new Node())) 
            //构造一个空node并CAS将其赋给head
                tail = head;//只有一个节点时,头尾节点相同
        } else {
            node.prev = t; //当前节点前驱连接tail
            //这步不需要CAS,因为node是线程自行构建的,别的线程无法修改
            if (compareAndSetTail(t, node)) { //CAS设置新的尾节点
                t.next = node;
                return t;
            }
        }
    }
   }
   
   /* 当同步节点成功添加进同步队列后,此方法进入一个自旋的个过程
    *每个节点(对应的线程)都在自省的观察,当条件满足,获取到同步状态,就从自旋状态退出
    *否则依然留在自旋过程中(并阻塞节点的线程),
    */
   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); //设置新节点
                //设置前驱结点的后继结点为null,使前驱结点成为不可达,方便回收
                p.next = null; // help GC
                failed = false;
                // 返回当前线程是否被中断
                return interrupted;
            }
            // 对前驱节点进行状态设置且调用park方法阻塞当前线程
            //
            if (shouldParkAfterFailedAcquire(p, node) && //设置前驱节点的状态
                parkAndCheckInterrupt())  //park阻塞(挂起)当前线程
                // 说明当前线程是被中断唤醒的。
                // 除非碰巧线程中断后acquire成功了,那么根据Java的最佳实践,
                // 需要重新设置线程的中断状态(acquire.selfInterrupt)
                interrupted = true;
            }
    } finally {
        if (failed) //获得同步状态
            cancelAcquire(node); //在等待队列中取消当前节点
    }
   }
   
    //将CANCELLED =  1(等待超时或者被中断的线程从等待线程出队);
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus; //前驱结点的同步状态
    if (ws == Node.SIGNAL)  //前驱被释放,后继被唤醒
        return true;
    // 过滤掉所有ws > 0的前驱节点,即CANCELLED = 1的节点
    // 节点在初始化时waitStatus值为0
    if (ws > 0) { //取消掉所有超时等待或者被唤醒的线程
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0); 
        pred.next = node;
    } else {
        // 将前驱结点的状态设置为Node.SIGNAL,表示其后继结点需要被唤醒
        // 0 或 PROPAGATE (CONDITION用在ConditonObject,这里不会是这个值)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        
    }
    // 返回为false让线程先不要阻塞
    // 再次判断前驱节点是否为首节点,并获取同步状态,如果获取失败,才阻塞线程
    return false;
  }


  //阻塞当前线程,直到被前驱唤醒或者被中断
  private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);  
    return Thread.interrupted(); //当前线程中断为标志
 }
 
public class LockSupport {
    //为了线程调度,在许可可用之前,禁用当前线程
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker); //设置阻塞状态
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    //设置阻塞状态
    private static void setBlocker(Thread t, Object arg) {
        // Even though volatile, hotspot doesn't need a write barrier here.
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }
}
释放同步方法分析:

当线程获取到同步状态并执行了相关逻辑之后,需要释放同步状态,并且使后续结点能够获取同步状态。调用同步器的release方法可以释放同步状态,该方法在释放同步状态后会唤醒其后继结点(使后继结点重新尝试获取同步方法)

释放同步状态相关源码:
  //独占式的释放同步状态,并且在释放同步状态后将同步队列中第一个节点包含的线程唤醒
    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,要删除节点为尾结点:将前驱置为尾节点
      * 2,当前节点为中间节点,且前驱后缀都需要获得同步状态 :前驱后缀相连
      * 3,非前两种情况,解除后面节点的阻塞状态
      */
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null; //将结点线程置为0;

        // Skip cancelled predecessors
        Node pred = node.prev;
		//删除所有当前结点前驱中所有线程状态被中断或者被取消的节点
        while (pred.waitStatus > 0) 从当前节点往前查找
            node.prev = pred = pred.prev;

        Node predNext = pred.next;//前驱的后缀

        node.waitStatus = Node.CANCELLED;  //当前结点状态置为结束状态

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) { 
			//当前结点为尾结点,并且将当前结点的前驱设为新的尾结点
            compareAndSetNext(pred, predNext, null); //将前驱的后缀(node)设为null
        } else {  //非尾结点
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
			//前驱和后继都需要获取同步状态,那么将他们两个连接在一起,达到删除当前结点的目的
            if (pred != head && 前驱不是头结点
                ((ws = pred.waitStatus) == Node.SIGNAL || 
                //前驱的waitStatus是-1(退出同步唤醒后继结点)
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                 //waitStatus不是1(线程已退出争夺同步状态)将其设置为SIGNAL
                pred.thread != null) { //前驱的结点不为空
                Node next = node.next; 
                if (next != null && next.waitStatus <= 0) 
                //当前结点有后继结点,并且当前结点处于获取同步状态
                    compareAndSetNext(pred, predNext, next); 
                    //将前驱和后继连接起来,达到删除当前结点的目的
            } else {
                unparkSuccessor(node);
                 //解除当前结点后一个结点的阻塞状态,从而达到删除当前结点的目的
            }

            node.next = node; // help GC
        }
    }
  

  //唤醒后继结点
  private void  unparkSuccessor(Node node) {
    int ws = node.waitStatus; //同步状态
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0); //将同步器设置为空闲状态
    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)  //<=表示线程仍然处于等待状态,
        //通过遍历找到离head最近的等待结点
            s = t;
    }
    if (s != null)  //s==null表示同步队列没有需要同步的线程了
        LockSupport.unpark(s.thread); //解除此线程的阻塞状态;
 }
unparkSuccessor(Node node)函数,为什么要从tail结点往前遍历唤醒需要的线程呢:
  • 如果正向寻找,当前头结点的后继结点可能会出现变为空的情况,如在setHeadAndPropagate()方法执行后会将老的头结点的后继结点置为null,方便GC回收,这样后继结点找不到则将无法找到状态小于0的后继结点,即使该结点是存在的。
  • 而尾结点倒序查询将确保能够找到该目标结点(前提是存在),因为尾结点的设置是线程安全的。
  • 在入队列的enq方法中,if(compareAndSetTail(t, node))和t.next = node这两行代码中间pred的next指针是为空的,而且如果unparkSuccessor方法从头部向后遍历中,判断某个节点的next指针是否为空的逻辑恰好在这两行代码之间,而某个节点恰好又是pred节点,所以就找不到真正需要unpark的节点,所以就导致了死锁,也就是后续节点永远不可能被唤醒。所以必须要从尾巴向前进行遍历,找到真正需要unpark的节点。

猜你喜欢

转载自blog.csdn.net/Fly_Fly_Zhang/article/details/89438543