AbstractQueuedSynchronizer与synchronized优缺对比及AQS 源码分析笔记

目前java有两种方式实现线程同步一种是synchronized方式 ,一种是基于AQS框架的方式;

AQS 同步和 synchronized 关键字同步是采用的两种不同的机制。

网上很多都在分析AQS的源码,基本都是在做源码的翻译注释工作,为什么要这样设计,这样设计的优缺点,却并没有讲到;

首先看下两者的优缺点:

synchronized 同步,synchronized 关键字经过编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令,这两个字节码需要关联到一个监视对象,当线程执行 monitorenter 指令时,需要首先获得获得监视对象的锁,这里监视对象锁就是进入同步块的凭证,只有获得了凭证才可以进入同步块,当线程离开同步块时,会执行 monitorexit 指令,释放对象锁,这个过程都是jvm自动为我们完成,并且为了优化synchronized,它还使用了偏向锁,轻量级锁,自旋锁,避免获取不到锁直接阻塞线程,以上方式获取锁失败,最后才升级为重量级锁阻塞线程,缺点是没有读写分离,无法控制释放锁的时机;


AQS同步则大量使用了“CAS更新volatile变量,不成功则重试”的方式来实现状态同步,避免了直接阻塞线程,减少了线程切换的开销,内部使用了变种的CLH队列锁,它是一种基于链表的高性能、公平队列锁,进入这个队列中的每个线程只会监视其前一个节点的状态,来判断自己是可以继续争抢锁还是需要阻塞,相比传统只使用一个变量来对所有线程做同步相比,可以减少多cpu的缓存同步开销(如果不明白为什么会减少缓存同步开销,请先了解一下jvm内存模型及其可见性在jvm中是如何实现的就明白我这句话说的什么了,简单来说就是所有线程监视一个变量时,会导致运行在不同cpu上的线程同时读写这一个变量,就需要额外的同步操作同步cpu缓存中的这个变量;而每个线程监视自己的变量,同一时刻只有一个cpu读写这个变量,就不存在cpu缓存同步开销了),具有更好的效率;AQS阻塞线程使用的是LockSupport实现的,持有锁的线程在释放锁的时候,进入CLH队列前,会先尝试使用CAS操作,去获取锁,无法马上获取到锁的线程会从CLH队列的队尾进入这个队列;如果是公平锁,则会直接进入队列;

 AQS的优点:


1. 如果锁能够快速释放,那么AQS同步器能最大限度的避免线程阻塞带来的上下问切换的开销,因为AQL同步器会在线程阻塞前使用cas操作尝试加锁,如果尝试失败,才会进入到阻塞状态,当同步块的执行时间小于线程切换开销,并且锁竞争不激烈的情况下,能大大提升性能;
2. AQS使用了变种的CLH队列,因为队列里的线程只监视其前面节点线程的状态,根据前面节点来判断自己是继续争用锁,还是需要被阻塞; 因为每个线程只会读写前一个线程的状态值,这个值只会被当前线程使用到,相比传统的所有线程都监视读写一个同步变量,CLH可以减少变量的变更带来的多处理器缓存同步的开销;


在 AQS 同步中,使用一个 int 类型的变量 state 来表示当前锁的状态,state在不同的同步方式下,用法不同;以独占式同步(一次只能有一个线程进入同步块)为例,state 的值大于等于0,其中 0 表示当前同步块中没有线程,大于0表示同步块中已经有线程在执行,state等于几代表这个线程加了几次锁。当线程要进入同步块时,需要首先判断 state 的值是否为 0,假设为 0,会尝试将 state 修改为 1,只有修改成功了之后,线程才可以进入同步块。

获取到锁的线程,在执行完同步代码后,释放锁,释放锁会将state最终设置为0,将当前持有所锁的线程设置为null,然后从CLH队列队头获取第一个park的线程,unpark它,被unpark的线程再去争用锁(为什么还要去争用,而不是直接获取锁呢?因为非公平模式下,新到来的线程会尝与被唤醒的线程争用锁);



以下对两种方式涉及到的类做对比:

 synchronized 方式AQS框架
同步方式 synchronized  同步块 ReentrantLock或ReentrantReadAndWriteLock的lock()、tryLock()、lockInterruptibly()和unlock()方法
释放锁等待 Object.wait()Condition.await()
唤醒等待线程Object.nobify(),Object.notifyAll() Condition.signal(),Condition.signalAll()

除此之外AQS框架还有一个CountDownLatch类,用于线程的同步;


AbstractQueuedSynchronizer 有一个同步队列Sync queue,CLH队列的变种,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度,它的作用是用来存放等待获取锁的jied。

除了同步队列,AbstractQueuedSynchronizer 还有Condition queue,不是必须的,它是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue,用来存放执行了Condition.await()的节点;


AQS是构建锁或者其他同步组件的基础框架,JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。AQS解决了子类实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列。基于AQS来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。AQS的使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。


AQS四个重要的成员变量

 

一、private transient Thread exclusiveOwnerThread;

   其父类父类AbstractOwnableSynchronizer的成员变量,代表当前持有独占锁的线程对象,有且只能有一个,其它线程只能进入到等待队列;

二、private volatile int state;

state代表加锁状态,不同的实现类state的作用不一样:

1.对于ReentrantLock是代表独占锁数量,无锁时state=0,有锁时state>0, 第一次加锁时,将state设置为1,持有锁的线程,可以多次加锁,只有持有锁的线程才可以多次加锁,经过判断加锁线程就是当前持有锁的线程时(即exclusiveOwnerThread==Thread.currentThread()),即可加锁,每次加锁都会将state的值+1,state等于几,就代表当前持有锁的线程加了几次锁,解锁时每解一次锁就会将state减1,state减到0后,锁就被释放掉了,其它线程又可以加锁了(所以加了几次锁就要解锁几次);当持有锁的线程释放锁以后,如果是公平锁,则等待队列会获取到加锁权限,在等待队列头部取出第一个线程去获取锁,获取锁的线程会被移出队列,如果是非公平锁,获取到加锁权限的有可能是等待队列中的第一个线程,也有可能是一个新加入的竞争锁的线程;

2.对于ReentrantReadAndWriteLock,state为4字节的整形,它的高位两个字节用来存储读锁数量(共享锁),低位两个字节用来存储写锁数量(独占锁),获取共享锁前要先判断有没有独占锁,如果存在读占锁,并且持有独占锁的线程不是当前线程,则获取锁失败;

3.对于CountDownLatch,state为初始化时传入的CountDown的次数,当state为0时,解除阻塞;

4.对于ThreadPoolExecuter的内部类Worker,state用来标识当前线程是否允许中断,Worker类就是线程池内运行的线程,它实现了Runable接口,继承了AbstractOwnableSynchronizer,state有3个值,-1代表Worker线程刚被实例化,还没运行,0代表线程已经运行,处于无锁状态,1代表线程已经加锁;线程的锁用于线程池在RUNNING和SHUTDOWN状态,Worker线程如果在执行任务,不允许被中断;

三、private transient volatile Node head;

等待获取锁的线程队列头节点


四、private transient volatile Node tail;

等待获取锁的线程的尾节点

 

 如果state>0,其它需要加锁的线程需要等待持有锁的线程释放锁,等待获取锁的线程会放入到一个先进先出的链表队列,headtail就是等待获取锁的队列的头节点和尾节点,除了头节点外,每个节点都与一个线程绑定(即在创建节点时,设置Node.thread = Thread.currentThread),等待获取锁的线程会通过调用Unsafe.park()方法阻塞等待;

持有锁的线程释放锁时会将state值为0,然后调用Unsafe.unpark()方法取消等待队列中头部的第一个线程的阻塞状态,去获取锁。

等待队列中的线程如果被park阻塞,这时其它线程中断了它,park就会解除阻塞,马上返回(这不是我们需要的,我们需要的是park方法一直阻塞,直到持有锁的线程释放了锁调用了unpark方法取消阻塞的返回),处于中断状态的线程 ,再调用park方法是无法阻塞的,所以这时必须要将线程状态重置为非中断状态,然后再次调用park方法阻塞等待,在线程获取到锁后再调用线程interrupt方法,恢复线程的中断状态;



四个重要的抽象方法


    //尝试获取独占锁,锁竞争时不一定能获取成功,成功则返回true,否则返回false

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


    //尝试释放独占锁,锁竞争时不一定能释放成功,成功则返回true,否则返回false
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }


    //尝试获取共享锁,锁竞争时不一定能获取成功,成功则返回true,否则返回false
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }


    //尝试释放共享锁,锁竞争时不一定能释放成功,成功则返回true,否则返回false
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }


上面这4个方法我们会发现都会抛出不支持的操作的异常,一般会在它的子类中重新实现这几个方法,原理都是通过调用UNSAFE类的CAS操作,来对state做加1操作(加锁)或减1操作(释放锁),因为是CAS操作,所以有可能失败;

我们一般实现这几个方法里只需要根据情况更改state和exclusiveOwnerThread的值,然后返回操作成功还是失败,同步过程则会由AQS帮我们实现;



加锁操作

public final void acquire(int arg) {

//如果尝试加锁失败,则调用AddWaiter方法将当前线程封装为Node对象,放入到等待加锁的队列的队尾,Node.EXCLUSIVE代表独占锁
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}


如果加锁失败,则将线程封装为一个Node对象放入到等待队列;

等待队列是由Node对象组成的一个双向链表,除了链表的头节点,每个Node都持有一个线程,以下是node的数据结构(Node是AQS的内部类);

static final class Node {

/**下面两个常量代表节点的类型**/


        //常量:代表Node节点是一个要获取共享锁的节点
        static final Node SHARED = new Node();
       //常量:代表Node节点是一个要获取独占锁节点
        static final Node EXCLUSIVE = null;


       /**下面4个常量代表Node节点的等待状态**/

       //代表该线程已经出现了异常后正等待获取锁超时,取消等待,这样的节点会被直接移出等待队列,放弃锁的争用;
        static final int CANCELLED =  1;
        /**代表该节点的后继节点包含的线程需要被执行,执行UNPARK操作,取消阻塞,去争用锁**/
        static final int SIGNAL    = -1;
        // 代表该节点处于Condition.await()状态 在condition队列中
        static final int CONDITION = -2;
表示当前场景下后续的acquireShared能够得以执行在condition队列中
        static final int PROPAGATE = -3;

        //节点的等待状态,值为上面4个常量的值,初始值为0,如果是头节点则也为0,值为0,表示当前节点在sync队列中,等待着获取锁
        volatile int waitStatus;


       //该节点的前一个节点
        volatile Node prev;


      //该节点的后一个节点
        volatile Node next;


       //当前节点封装的等待获取锁的线程,如果该节点是链表的头节点,则Thread为null
        volatile Thread thread;


      //共享锁的下一个节点
        Node nextWaiter;


        //判断次节点是否是等待共享锁
        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.await()队列
        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }



以下是入队过程:

 //将当前线程封装为Node对象,如果队列已经初始化,使用cas操作尝试将节点放入等待队列队尾;如果队列为空或cas入队失败,则执行enq方法,enq方法中如果队列没有初始化,则会初始化队列,然后使用cas操作将对象入队;

 private Node addWaiter(Node mode) {

        //将当前线程封装为一个node节点

        Node node = new Node(Thread.currentThread(), mode);

        // Try the fast path of enq; backup to full enq on failure

        //获取尾节点

        Node pred = tail;

        //如果队列已经初始化,则尝试将node放入到队列尾部,

        if (pred != null) {

            //将当前节点的前继节点指向尾节点

            node.prev = pred;

            //使用cas操作将尾节点指向当前节点

            if (compareAndSetTail(pred, node)) {

                 //将尾节点的后继节点指向自己

                pred.next = node;
                return node;//如果入队成功,返回Node
            }
        }

       //如果队列没有初始化,或入队失败,则采用自旋锁的方式,将node加进队列;

       //CLH队列是懒初始化的

        enq(node);
        return node;
    }

//for循环,如果队列为空,则初始化队列(初始化头结点head,然后将tail指向head),否则使用cas将节点入队

//自旋锁,将node加入到队列尾部

private Node enq(final Node node) {

        for (;;) {

            //获取尾节点

            Node t = tail;

           //如果尾节点为null,说明队列为空,且还没有初始化,需要要初始化队列
            if (t == null) {

                 //使用cas方式尝试设置头结点为一个新的Node节点;
                if (compareAndSetHead(new Node()))

                    tail = head; //如果设置头结点成功,则将尾节点指向头节点;

               //如果设置失败,说明有其它需要入队等待的线程已经初始化了队列;则继续循环

            } else {//如果尾节点不是null 说明队列不为空

                //将当前节点的前继节点指向尾节点

                node.prev = t;

                if (compareAndSetTail(t, node)) {//使用cas将尾节点指向当前节点

                    //将尾节点后继节点指向自己

                    t.next = node;
                    return t;
                }
            }
        }
    }

    //进入队列后的操作,如果其前一个节点为头结点,则尝试获取锁,如果获取成功,则将当前节点设置为头结点后,直接返回线程中断状态,否则查看其前一个节点的状态,根据前一个节点的状态决定是否要执行park状态阻塞自己;

    // sync队列中的结点在独占且忽略中断的模式下获取(资源)
    final boolean acquireQueued(final Node node, int arg) {
        // 标志
        boolean failed = true;
        try {
            // 中断标志
            boolean interrupted = false;
            for (;;) { // 无限循环
                // 获取node节点的前驱结点

                final Node p = node.predecessor(); 

                // 当前节点的前继节点为头结点,则说明它的前面没有等待节点,于是就尝试去获取锁,如果获取锁成功,则将当前节点设置为头结点;

                if (p == head && tryAcquire(arg)) { 

                    // 设置头结点为当前节点

                    setHead(node); 

                    //设置旧的头结点的后继节点为null,断开链表,帮助垃圾回收器回收该对象,否则垃圾回收器还会扫描其next指向的对象是否也是垃圾对象

                    p.next = null; // help GC
                    failed = false; // 设置标志
                    return interrupted; 

                }

                // 如果当前节点前继节点不是头节点,或者是头结点,但是尝试获取锁失败

                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }

    }


  1. CANCELLED,值为1,表示当前的线程取消竞争锁的操作,可能是等待超时的原因,也可能是发生了异常;
  2. SIGNAL,值为-1,表示当前节点的后继节点包含的线程等待着被unpark,去竞争锁;
  3. CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中;
  4. PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行;
  5. 值为0,表示当前节点在sync队列中,等待着获取锁。

   // 当获取锁失败后,检查并且更新结点状态
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取前驱结点的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            // 可以进行park操作
            return true; 
        if (ws > 0) { // 表示前继节点状态为CANCELLED,为1 ,如果前继节点都取消了,那么当前节点就不能被park,需要返回false后,继续执行循环;
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */

            do {

                //将当前节点的前继节点,指向前继节点的前继节点;

                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点
            // 赋值pred结点的next域
            pred.next = node; 
        } else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中),前继节点调用await释放了锁,那么当前节点就不可以被park 
            /*
             * 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.
             */
            // 比较并设置前驱结点的状态为SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
        }
        // 不能进行park操作
        return false;

    }


     // 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();

}

如果当前线程被中断,park方法则会取消阻塞状态,当前线程则继续往下执行,通过 Thread.interrupted()方法清理中断状态,并返回清理前的中断状态,返回后就又到了acquireQueued方法的for循环中;

解锁操作

持有锁的线程执行完同步块后,会执行以下代码释放锁;

    /**

     * 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}.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */

    public final boolean release(int arg) {

        //执行tryRelease操作释放锁,这个方法需要我们自己去实现

        if (tryRelease(arg)) {

            //如果释放锁成功,如果head不为null,说明队列已经被初始化,并且head的waitStatus不为0,说明后继节点有被park的等待的,则执行

            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;

    }


  /**

  

    ****/

   /** 有两个地方会调用这个方法,一个是释放锁,唤醒头部第一个不为取消节点的线程,第二个是节点取消等待
     * Wakes up node's successor, if one exists.
     *
     * @param node the node
     */
    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;

        // 如果head节点waitStatus<0, 将其修改为0

        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.

         */

//如果当前节点没有后继节点,或者当前节点后继节点状态为,状态为CANCELLED 状态,则从队尾往前找,找到队列中最靠近队头的第一个非CANCELLED节点

        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);
    }



取消等待


 /**
     * Cancels an ongoing attempt to acquire.
     *
     * @param node the node
     */
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        //将当前节点线程值为null;
        node.thread = null;

        

        // Skip cancelled predecessors

        //缓存当前节点的前置节点

        Node pred = node.prev;

        //如果前置节点状态是CANCELLED,那么则将当前节点的前置节点设置为前面第一个不是CANCELLED状态的节点,并将pred指向这个不为CANCELLED状态的节点;

        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        //这时候pred指向的是当前节点前面第一个状态不为CANCELLED的节点

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel

        // or signal, so no further action is necessary.

        //缓存前置节点的下一个节点,如果当前节点前置节点不是CANCELLED状态的,那么这个节点就是当前节点自己

        Node predNext = pred.next;

        //将当前节点设置为CANCELLED状态
        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

       //如果当前节点是尾节点,并设置其前置节点(前面第一个不为CANCELLED状态的节点)设置为尾节点成功
        // If we are the tail, remove ourselves.

        if (node == tail && compareAndSetTail(node, pred)) {

            // 设置pred结点的next节点为null

            compareAndSetNext(pred, predNext, 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;

            //如果同时满足以下三个条件

            //1.如果前置节点不是头节点并且 

            //2.前置节点状态为SIGNAL ,或者 1.前置节点状态不为CANCELLED 2. 设置前置节点状态为SIGNAL成功 (也就是说必须让前置节点状态为SIGNAL)

            //前置节点的线程不为null

            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&

                pred.thread != null) {

                //缓存当前节点下一个节点

                Node next = node.next;

                //如果当前节点的后继节点不为null,并且状态不为CANCELLED ,则将前继节点的next指向后继节点,断开前面的链表;

                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            //将当前节点的下一个节点设置为自己
            node.next = node; // help GC
        }
    }


未完待续;



  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值