AQS(AbstractQueuedSynchronizer)源码初识

        前几天在对比Synchronized和ReentrantLock的关系和区别时,以及学习使用Semaphore、CountDownLatch和CyclicBarrier时,发现里面底层都有这样一个同步器。这让我觉得学习它们的底层原理,就不得不学习AQS自身的底层原理,那么,我们就来吧。

        这里参考了队列同步器(AQS)详解 和 这才是图文并茂:我写了1万多字,就是为了让你了解AQS是怎么运行的

目录

成员变量

Node

独占式同步组件的设计

同步器提供的模板方法

供子类重写的方法

具体方法实现

acquire方法

Release


成员变量

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 7373984972572414691L;

    /**
		创建一个新的AbstractQueuedSynchronizer实例,初始同步状态为零。
     */
    protected AbstractQueuedSynchronizer() { }
  
    
    static final class Node{...} //数据结构中的节点,下面细说。
  
  
    /**
		等待队列的头部,延迟初始化。 除初始化外,仅通过 setHead 方法进行修改。 注意:如果 head 存在,则保证其     waitStatus 不会被 CANCELLED。
     */
    private transient volatile Node head;

    /**
		等待队列的尾部,延迟初始化。 仅通过方法 enq 修改以添加新的等待节点
     */
    private transient volatile Node tail;

    /**
		同步状态。
     */
    private volatile int state;
  
}

        可以看出同步器本身其实就是一个双向链表(行为约束上,更像一个双端队列),我们直接能获取的节点只有head和tail。主要是我们要考察这个节点Node的含义。其中head节点为正在运行线程的的节点。

         

Node

static final class Node {
        /** 指示节点在共享模式下等待的标记 */
        static final Node SHARED = new Node();
        /** 指示节点正在以独占模式等待的标记 */
        static final Node EXCLUSIVE = null;

        /** 指示线程已取消的 waitStatus 值*/
        static final int CANCELLED =  1;
        /** 指示后继节点需要被唤醒的waitStatus 值 */
        static final int SIGNAL    = -1;
        /** waitStatus 值指示线程正在等待条件(进入等待队列) */
        static final int CONDITION = -2;
        /**指示下一个acquireShared 应无条件传播的waitStatus 值*/
        static final int PROPAGATE = -3;


        volatile int waitStatus;

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

        /**
		  后继节点
         */
        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() {    // 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节点等待状态的字段waitStatus,取值总共有CANCELLED(-1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0,这5个值代表了不同的特定场景。

  • CANCELLED:表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL:表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL(记住这个-1的值,因为后面我们讲的时候经常会提到)
  • CONDITION:表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。(注:Condition是AQS的一个组件,后面会细说)
  • PROPAGATE:共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

当waitStatus为负值表示结点处于有效等待状态,为正值的时候表示结点已被取消。

独占式同步组件的设计

同步器提供的模板方法

//独占式获取同步状态,如果当前线程获取同步状态成功,立即返回。否则,将会进入同步队列等待,
//该方法将会重复调用重写的tryAcquire(int arg)方法
public final void acquire(int arg) {...}


//与acquire(int arg)基本相同,但是该方法响应中断。
public final void acquireInterruptibly(int arg){...}



//独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒
public final boolean release(int arg) {...}

供子类重写的方法

//独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态
protected boolean tryAcquire(int arg)
 
//独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
protected boolean tryRelease(int arg)
 
//当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
protected boolean isHeldExclusively()

具体方法实现

acquire方法

/**
以独占模式获取,忽略中断。 通过至少调用一次tryAcquire ,成功返回。 否则线程会排队,可能会反复阻塞和解除阻塞,调用tryAcquire直到成功。 此方法可用于实现方法Lock.lock 。
*/
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && //如果tryacquire失败
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//生成节点并加入同步队列
            selfInterrupt();
    }

/**
为当前线程和给定mode创建节点并排入队列
*/
private Node addWaiter(Node mode) { //共享/独占性,null为独占
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) { //
            node.prev = pred;//连接到尾部之后
            if (compareAndSetTail(pred, node)) { //CAS设置,期望当前尾部是pred,是的话改为node。
               //如果CAS尝试成功,就说明"设置当前节点node的前驱"与"CAS设置tail"之间没有别的线程设置tail								成功
                //只需要将"之前的tail"的后继节点指向node即可
                pred.next = node;
                return node;
            }
        }
        enq(node);//否则,通过死循环来保证节点的正确添加
        return node;
    }


/**
每个节点以死循环方式来判断自身是否头结点。

*/
    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) //获取锁失败,则将此线程对应的node的waitStatus改为CANCEL
                cancelAcquire(node);
        }
    }


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 前驱结点等待状态为"SIGNAL",那么自己就可以安心等待被唤醒了
            return true;
        if (ws > 0) {
            /*
           * 前驱结点被取消了,通过循环一直往前找,直到找到等待状态有效的结点(等待状态值小于等于0) ,
           * 然后排在他们的后边,至于那些被当前Node强制"靠后"的结点,因为已经被取消了,也没有引用链,
           * 就等着被GC了
           */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
             // 如果前驱正常,那就把前驱的状态设置成SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

        上述逻辑可由下图表示:

         当前线程获取同步状态失败时,同步器会将当前线程、等待状态等信息构造成一个Node并加入同步器队列中,同时会阻塞当前线程。

同时 acquireQueued 会进行如下操作:

1、CAS自旋,先判断当前传入的Node的前结点是否为head结点,是的话就尝试获取锁,获取锁成功的话就把当前结点置为head,之前的head置为null(方便GC),然后返回

2、如果前驱结点不是head或者加锁失败的话,就调用shouldParkAfterFailedAcquire,将前驱节点的waitStatus变为了SIGNAL=-1,最后执行parkAndChecknIterrupt方法,调用LockSupport.park()挂起当前线程,parkAndCheckInterrupt在挂起线程后会判断线程是否被中断,如果被中断的话,就会重新跑acquireQueued方法的CAS自旋操作,直到获取资源。

ps:LockSupport.park方法会让当前线程进入waitting状态,在这种状态下,线程被唤醒的情况有两种,一是被unpark(),二是被interrupt(),所以,如果是第二种情况的话,需要返回被中断的标志,然后在acquire顶层方法的窗口那里自我中断补上

可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO。并且也便于对过早通知的处理(过早通知是指:前驱节点不是头节点的线程由于中断而被唤醒)。 

Release

        head节点的线程在释放同步状态时,将会唤醒后续节点,后续节点将在获取同步状态成功时将自己设置为head节点。

public final boolean release(int arg) {
        if (tryRelease(arg)) { //释放同步状态成功的话
            Node h = head;
            if (h != null && h.waitStatus != 0) //独占即为SIGNAL -1 要唤醒后面的
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


private void unparkSuccessor(Node node) {

        int ws = node.waitStatus; //当前节点waitStatus
        if (ws < 0)
             //将head结点的状态置为0
            compareAndSetWaitStatus(node, ws, 0);//更新为0
        //找到下一个需要唤醒的结点s
        Node s = node.next;
        //如果为空或已取消
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从后向前,直到找到等待状态小于0的结点,前面说了,结点waitStatus小于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); //唤醒
    }

        方法逻辑就是,先将head的节点状态置为0,避免下面找节点的时候再找到head,然后找到队列中最前面的有效节点,然后唤醒,我们假设这个时候线程A已经释放锁,那么此时队列中排最前边竞争锁的线程B就会被唤醒。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值