实现显式锁的关键类——AbstractQueuedSynchronizer队列同步器(AQS)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zy1994hyq/article/details/83656109

概述

同步器内部其实就是用双向链表来存储线程,关于锁的相关操作都可以通过操作这个链表实现。

同步器依赖内部的同步队列(FIFO)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点并将其加入到同步队列,同时阻塞当前线程。首节点是获取同步状态成功的节点,同步状态释放时,首节点会唤醒后继节点中的线程,并可能让其获取同步状态。

同步器拥有的头节点(head)、尾节点(tail), 还有一个线程同步队列,以双向链表的形式体现。

关于同步队列

线程在尝试获取同步状态的两种情况

  1. 如果锁的实现是公平锁,则需要判断同步队列中有没有等待锁的节点,没有则当前线程可以获取同步状态,否则AQS会把该线程构造成一个节点,即AbstractQueuedSynchronizer中的内部类Node类,然后让内部的tail节点(末节点)引用该节点,将生成的节点加入到等待队列中,此阶段是通过compareAndSetTail方法利用CAS原理设置tail指向该节点的引用。
  2. 如果锁的实现是非公平锁,当前线程先尝试获取锁,如果能够成功获取到锁,则不需要加入到等待队列,否则与同步队列一样加入到等待队列中

等待队列中的线程获取同步状态

获取到同步的线程,AQS中的head节点会指向包含该线程的节点,执行完相应的逻辑后,会释放同步状态。然后首节点会唤醒它的后继节点(next引用)并让该节点中的线程参与获取同步状态的活动。 

AbstractQueuedSychronizer内部节点类Node的部分源码: 

    static final class Node {
         //读写锁ReentrantReadWriteLock中,可以多个读线程同时获取锁,就是利用了共享模式
        //共享类型节点,表示在共享模式下的等待节点
        static final Node SHARED = new Node();
        //独占类型节点,表示在独占模式下的等待节点
        static final Node EXCLUSIVE = null;

        //等待状态,取消,表示该节点存放的线程被取消,CANCELLED状态的节点应该被移除节点链表
        static final int CANCELLED =  1;
        //等待状态,发信号(通知),当前节点为SIGNAL状态时,表示后继节点应该是阻塞的(park)
        //当前节点释放同步状态后会通知后继节点,唤醒后继节点阻塞的线程
        static final int SIGNAL = -1;
        //等待状态值,表示当前节点在等待condition,也就是在condition队列(条件队列也叫等待队列)中
        static final int CONDITION = -2;
        //等待状态值,传播。使用在共享模式中的一种特殊状态,表示无条件向后传播唤醒动作,
        //下一次共享模式时,获取状态的操作应该无条件的传播
        static final int PROPAGATE = -3;
        //当前的等待状态,取值为5种,包括上面定义的CANCELLED、SIGNAL、CONDITION、PROPAGATE,        
        //除此之外还有  0 :表示除上面四种之外的情况
        //总的来说状态可以简单分为就3种:取消、阻塞、执行
        //CANCELLED 取消,SIGNAL、CONDITION、PROPAGATE 独占阻塞/条件阻塞/共享阻塞
        volatile int waitStatus;//int类型默认情况下为0,这里可以表示节点初始状态或者正在执行

        //当前节点的前一个节点
        volatile Node prev;
        //当前节点的后一个节点,注意与nextWaiter区分,nextWaiter是用于condition队列中后继节点的指向
        volatile Node next;
        //当前节点存放的线程
        volatile Thread thread;

        //使用在condition队列中的后继节点。
        Node nextWaiter;

        //如果节点实在共享模式中 返回true
        final boolean isShared() {
            return nextWaiter == SHARED;
        }
        
        Node() {}
        //提供给addWaiter方法使用,生成同步队列的节点
        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;
        }

    }

AbstractQueuedSychronizer定义的变量:

    //同步队列的头结点,除了初始化时设置值之外就只能通过setHead方法设置值,头结点所对应的含义是当前占有同步状态(锁)且正在运行
    private transient volatile Node head;
    //同步队列的末节点
    private transient volatile Node tail;
    //同步状态,在显示锁的实现里也用来表示当前占有锁的线程数量
    private volatile int state;

怎么实现同步状态的管理?

事实上显示锁(ReentrantLock)、信号量(Semaphore)、计数器(CountDownLatch)的实现,都是采用模板方法模式实现的,AbstractQueuedSynchronizer就是模板类。

下面就是具体实现类需要实现的方法:

boolean tryAcquire(int arg)	试获取独占锁
boolean tryRelease(int arg)	试释放独占锁
int tryAcquireShared(int arg)	试获取共享锁
boolean tryReleaseShared(int arg)	试释放共享锁
boolean isHeldExclusively()	当前线程是否获得了独占锁

下面是在模板类AQS中已经实现的具体方法:

void acquire(int arg)	获取独占锁。会调用tryAcquire方法,如果未获取成功,则会进入同步队列等待
void acquireInterruptibly(int arg)响应中断版本的acquire,其实这个方法与acquire方法的区别主要就是,这个方法抛出了中断异常
boolean tryAcquireNanos(int arg,long nanos) 超时+响应中断版本的acquire,这个方法主要除了会抛出异常,还加入了超时的判断
void acquireShared(int arg)	获取共享锁。会调用tryAcquireShared方法
void acquireSharedInterruptibly(int arg)	响应中断版本的acquireShared
boolean tryAcquireSharedNanos(int arg,long nanos)	响应中断+带超时版本的acquireShared
boolean release(int arg)	释放独占锁
boolean releaseShared(int arg)	释放共享锁
Collection<Thread> getQueuedThreads()	获取同步队列上的线程集合

在解析上诉方法源码之前,还需要看一下Java中关于线程中断方法的解释:

Thread.interrupt()的作用是中断本线程。
本线程中断自己是被允许的;其它线程调用本线程的interrupt()方法时,会通过checkAccess()检查权限。这有可能抛出SecurityException异常。

如果由于调用线程的wait(), wait(long),wait(long, int),join(), join(long), join(long, int), sleep(long), sleep(long, int)使得本线程是处于阻塞状态。若线程在阻塞状态时,调用了它的interrupt()方法,那么它的“中断状态”会被清除并且会收到一个InterruptedException异常。

如果这个线程在一个可中断通道的I/O操作中被阻塞,并且这个通道将被关闭,线程的中断状态将会被设置,线程将收到一个ClosedByInterruptException。

如果线程被阻塞在一个Selector选择器中,那么通过interrupt()中断它时;线程的中断标记会被设置为true,并且它会立即从选择操作中返回。
如果不属于前面所说的情况,那么通过interrupt()中断线程时,它的中断标记会被设置为“true”。
中断一个“已终止的线程”不会产生任何操作。  

由于在源码中很多地方都用到了CAS算法,这里就简单解释一下:

CAS算法有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这个内存值就是根据valueOffset得到的内存地址里的值。 
       compareAndSwapObject(Object var1, long var2, Object var3, Object var4)
         var1 操作的对象
         var2 内存地址
         var3 根据var2得到对应内存地址里的值,将该内存值的与var3比较,相等才更新
         var4 更新值
         更新成功 返回true, 反正返回false

具体方法源码解析

首先看向节点链表里面新增节点的方法enq:

    //向节点队列中新增节点
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;//令 t 指向 末节点
            //末节点为空
            if (t == null) { 
                //通过CAS算法给头结点设置一个无意义的节点,如果CAS返回true,则设置末节点等于头结点
                //从这里可以看出队列初始状态下头结点与末节点相同,都为一个无实际意义的节点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {    
                //新增的节点node添加到链表末尾
                //这里为什么不把node.prev=t放到的if里呢?
                //试想一下CAS在设置tail为node的一瞬间,此时node节点的前置节点和后置节点都为控,
                //也就是说这一瞬间造成tail从链表队列中断开
                //而如果将node.pre=t放在if之前,就可以保证在CAS在设置tail为node时就已经和链表链接起来了
                //所以后面一些方法遍历链表也是从末节点向前反向遍历的
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    //CAS算法设置头结点,判断头节点是否为空,如果为空设置头结点为 update
    private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
    }
    //CAS算法设置末节点,判断末节点是否等于expect,如果为空设置头结点为 update
    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
   //CAS算法设置状态值,如果当前状态值等于预期值,则原子性地(避免多线程引起并发问题)将同步状态设置为给定的更新值。
    //这个操作具有{volatile}读写的内存语义。
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

 cancelAcquire方法,取消正在尝试获取的节点方法源码:

    //移除node节点节点
    private void cancelAcquire(Node node) {
        // 如果节点不存在
        if (node == null)
            return;
        //先将节点存放的线程是置为空
        node.thread = null;

        //向前查找,找到状态不为CANCELLED的节点,代码 node.prev=pred=pred.prev 可以让状态为CANCELLED的节点从链表中移除,
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        
        Node predNext = pred.next;//这一行代码和之前的while循环里的代码的作用就是,移除状态为CANCELLED的节点
        node.waitStatus = Node.CANCELLED;
        //如果node为末节点,则设置末节点为node的前置节点,然后将node从链表中移除
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            /** 如果node不是末节点有两种情况
              * 1.node既不是尾,也不是头的后继节点,即node的前置节点不是head节点,
              *   并且node的前置节点的状态为SIGNAL或者可以设置为SINGAL,并且node的前置节点中的线程不能为空
              * 2.node是头的后继节点,即node的前置节点就是head节点,
              *   或者node前置节点状态不是SINGAL并且不能设置为SINGAL,再或者node的前置节点中的线程为空
              */  
            int ws;
            if (pred != head && 
                    ((ws = pred.waitStatus) == Node.SIGNAL || 
                    (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL)))
                    && pred.thread != null) {
                //第一种情况下,做链表移除节点的处理,将node的前置节点的next节点设置为当前node节点的next节点 
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                //第二种情况下,唤醒node节点的后继节点
                unparkSuccessor(node);
            }
            //此操作后node节点就从链表中移除了
            node.next = node; // help GC
        }
    }

cancelAcquire方法主要的目的是将node节点从同步队列中移除。 

唤醒节点node的后继节点,unparkSuccessor唤醒后继节点方法源码  

    //唤醒节点node的后继节点
    private void unparkSuccessor(Node node) {
        //如果节点的状态小于0,即为:SINGAL、CONDITION、PROPAGATE时,将状态改为 0
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        
        Node s = node.next;
        //如果node的后继节点为空或者状态为CANCELLED,则从末节点开始反向查询可以唤醒的节点
        //有两种情况下回出现node的后继节点为空:
        // 1. node为末节点
        // 2. 上面在解析enq方法说到的,在使用enq方法插入节点时,当代码还没有进入到if代码块里的时候,就出现了后继节点为空的情况。  
        //        node.prev = t;
        //        if (compareAndSetTail(t, node)) {
        //            t.next = node;
        //            return t;
        //        }
        //                    
        //       ----       ----       ----
        //      |    |<----|    |<----|    |
        //      |    |---->|    |     |    |  此时仅仅执行了node.prev=t;还没有执行if的内容
        //       ----       ----       ---- 
        //下面if里面的语句就是保证s节点就是node的后继节点中第一个不为CANCELLED状态的节点       
        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;
        }
        //唤醒节点s中的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

unparkSuccessor方法主要做了两件事:

1. 设置节点 node 的状态为 0,表示node里的线程正占有同步状态,正在执行

2. 唤醒node的后继节点,告诉后继节点等着同步状态,后继节点会通过循环的方式获取同步状态。

独占模式下获取获取同步状态,通过acquire方法,acquire(int arg)方法及其具体过程涉及的方法源码:

    //获取同步状态(获取锁的操作其实就是获取同步状态)
    public final void acquire(int arg) {
        //如果tryAcquire()方法获取同步状态失败,则调用addWaiter方法将当前线程构成Node节点加入到同步队列中
        //然后通过acquireQueued方法从节点同步队列中获取加入的节点,
        //如果获取到的节点中的线程为中断状态,则acquireQueued会返回true
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//中断当前线程
    }

    //将当前线程构成节点加入到同步队列中
    private Node addWaiter(Node mode) {
        //生成Node节点,mode用于给Node中nextWaiter赋值,nextWaiter只会在condition队列中使用
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;//如果末节点不为空,则直接将新生成的节点node添加在最后成为末节点,
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果末节点为空,调用enq方法循环尝试插入节点。本文上面有enq方法的源码解析。
        enq(node);
        return node;
    }

    //从节点同步队列中获取节点
    //这个方法的作用主要自旋获取同步状态
    //返回当前线程的中断状态
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //如果node的前置节点为头节点,并且获取到同步状态(锁),
                //证明此时头结点里的线程已经没有占有锁了,此时设置头结点为node节点,并且将原来的头结点 p 从链表中断开
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                } 
                //根据条件判断是否应该阻塞自己,如果node的前置节点p为SIGNAL状态,则阻塞当前线程,
                //并且判断当前线程是否被中断,如果是则设interrupted为true
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //经过上面的判断,如果是正常跳出for循环的,那么failed最后一定为false
            //因此这里failed为true的情况只有可能是上面的代码发生异常了导致没有for循环没有正常结束
            if (failed)
                cancelAcquire(node);
        }
    }

    //获取锁失败后,是否应该进行阻塞线程的操作
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果前置节点的状态为SIGANAL,则后面的节点应该阻塞,直到前置节点释放了同步状态时才唤醒后继节点,因此这里返回true
        if (ws == Node.SIGNAL) 
            return true;
        if (ws > 0) {
            //如果ws>=0,也就是pred的状态为CANCELLED,那么从pred开始向前搜索,
            //移除CANCELLED状态的节点,直到遇见状态waitState>=0的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果前置节点pred的状态为0或者PROPAGATE时,设置pred节点的状态为SIGNAL
            //独占模式下前置节点pred的状态为0
            //共享模式下前置节点pred的状态为0或者PROPAGATE
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //根据当前对象的this引用,阻塞当前线程,并且返回当前线程是否中断
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

acquire方法涉及到的具体流程:

  1. 首先通过tryAcquire获取同步状态(锁)
  2. 如果失败,就将当前线程生成一个Node节点添加到同步队列的链表中并阻塞线程 
  3. 继续通过acquireQueued方法从同步队列中获取上一步添加到同步队列中的节点,在acquireQueued方法中通过自旋获取同步状态
  4. 如果前置节点是头结点,尝试获取同步状态,如果获取到同步状态,则将头结点设为node节点,将之前的头结点从链表中移除。
  5. 如果前置节点不是头结点,或者没有获取到同步状态,线程会在acquireQueued方法内部被阻塞,在同步队列中等待唤醒,由于acquireQueued方法内部是自旋的方式通过循环尝试获取同步状态,因此当阻塞节点的线程被唤醒后会继续尝试获取同步状态。

流程图如下:

释放独占模式下的同步状态(锁):

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //此时头结点可能出现的情况
            //1. 头结点为空,还没有初始化,初始情况下,head = null,当第一个节点入队后,head 会被初始为一个虚拟节点。
            //   这里,如果还没节点入队就调用 release 释放同步状态,就会出现 h = null 的情况。
            //2. 头结点状态为0,头结点中的线程正在执行,不需要唤醒后继节点
            //3. 头结点不为空,头结点中的线程没有执行,需要唤醒后继节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

正在执行的线程所在节点的状态为0,当线程执行结束后,释放同步状态时必须更改同步状态,因此在具体实现tryRelease的方法中必须修改执行完的线程所在节点的状态,否则在AQS的release方法中会造成后继节点无法被唤醒的情况。这并不是AQS的问题,而是具体实现线程管理的类在重写tryRelease方法时可能会出现的Bug。

共享模式

共享模式中允许多个线程同时获取同步状态(锁),读写锁(ReetrantReadWriteLock)中的读锁就是通过共享模式实现的。计数器(CountDownLatch) 和 信号量(Semaphore)的实现也是依赖于共享模式。

共享模式下获取获取同步状态,通过acquireShared方法,acquireShared(int arg)方法及其具体过程涉及的方法源码:

    public final void acquireShared(int arg) {
        //尝试获取共享同步状态,如果小于0
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

    private void doAcquireShared(int arg) {
        //将当前线程封装成节点加入同步队列,节点的类型为Node.SHARED
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    //尝试获取共享同步状态
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        //设置node节点为头节点,唤醒共享类型的后置节点
                        setHeadAndPropagate(node, r);
                        //下面的部分与独占模式中一样,这里不再重复解析
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                //下面与独占模式一样
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    //设置node节点为头节点,唤醒共享类型的后置节点
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; 
        setHead(node);
        //在共享模式下,waitStatus只有两种小于0的状态,也就是waitStatus为SIGNAL(-1)或PROPAGATE(-3)两种情况
        //而waitStatus等于PROPAGATE(-3)是调用doReleaseShared方法时,头结点的状态为0时设置的
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            // 释放后继节点的两种情况
            // 1. s为空,上面unparkSuccessor方法解析已经解释了s为空的两种情况,这里不再重复
            // 2. s为共享类型节点
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
    //这个方法主要的作用是唤醒下一个线程或者设置传播状态。
    //后继线程被唤醒后,会尝试获取同步状态(锁),如果成功之后,则又会调用setHeadAndPropagate,将唤醒传播下去。
    private void doReleaseShared() {
        for (;;) {
            //在setHeadAndPropagate方法中的传入参数node节点,通过setHead方法设置node为头结点,
            //因此这里的head节点就是setHeadAndPropagate方法中的node节点
            Node h = head;
            //如果头节点不为空,并且头结点不等于末节点,即头结点之后还有其他的节点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //头结点的状态为SIGNAL,则唤醒后继节点,并将头结点的状态设为0
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    unparkSuccessor(h);
                }
                //此时头结点状态为0正在执行,设置状态为PROPAGATE,setHeadAndPropagate 在读到 h.waitStatus < 0 时,
                //可以继续唤醒后面的节点。这个状态是为了解决一个BUG出现的,在下面会有说明
                else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // 如果头节点改变,跳出传播循环
                break;
        }
    }

    //获取锁失败后,是否应该进行阻塞线程的操作
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果前置节点的状态为SIGANAL,则后面的节点应该阻塞,直到前置节点释放了同步状态时才唤醒后继节点,因此这里返回true
        if (ws == Node.SIGNAL) 
            return true;
        if (ws > 0) {
            //如果ws>=0,也就是pred的状态为CANCELLED,那么从pred开始向前搜索,
            //移除CANCELLED状态的节点,直到遇见状态waitState>=0的节点
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果前置节点pred的状态为0或者PROPAGATE时,设置pred节点的状态为SIGNAL
            //独占模式下前置节点pred的状态为0
            //共享模式下前置节点pred的状态为0或者PROPAGATE
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

共享模式关键点:doReleaseShared方法,该方法可以唤醒共享类型的后继节点,这样的结果是,如果当前占用同步状态(锁)的线程释放了同步状态,则这个时候在同步队列中,前面通过doReleaseShared方法唤醒的多个节点将会一起获取到同步状态(锁), 实现了锁的共享。这就是读写锁中读读不互斥具体的实现原理。

acquireShared方法涉及到的具体流程:

  1. 首先通过tryAcquireShared获取同步状态(锁)
  2. 如果失败,就将当前线程生成一个Node节点添加到同步队列的链表中 
  3. 通过自旋的方式尝试获取同步状态
  4. 如果前置节点是头结点,尝试获取同步状态,如果获取到同步状态,则将头结点设为node节点,并且唤醒共享类型的后继节点,将之前的头结点从链表中移除。
  5. 如果前置节点不是头结点,或者没有获取到同步状态,线程会在doAcquireShared方法内部被阻塞,在同步队列中等待唤醒,由于doAcquireShared方法内部是自旋的方式通过循环尝试获取同步状态,因此当阻塞节点的线程被唤醒后会继续尝试获取同步状态。

可以看出其实共享模式和独占模式获取同步状态的基本流程是一样的,唯一的不同在与,共享模式需要唤醒共享类型的后继节点。

共享模式下流程图如下:

共享模式下释放同步状态

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

可以看出共享模式下释放同步状态也会调用 doReleaseShared 方法。共享类型的节点线程在获取同步状态和释放同步状态时都会调用 doReleaseShared。这一情况在早期AQS共享模式下获取同步状态的源码中会引发了一个bug,而为了解决这个Bug,就引入Node.PROPAGATE,下面就来解释一下PROPAGATE状态存在的意义。

PROPAGATE状态存在的意义

首先来看一下在早期setHeadAndPropagate与releaseShared方法的源码:

private void setHeadAndPropagate(Node node, int propagate) {
    setHead(node);
    if (propagate > 0 && node.waitStatus != 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            unparkSuccessor(node);
    }
}

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

与现在的代码对比可以发现:

1. 在setHeadAndPropagate方法中,现在的代码关于状态waitStatus的判断从不等于0改为了小于0

2. 以前的releaseShared方法与独占模式下release方法是一样的,而现在在共享模式下释放同步状态和唤醒后继节点都放在了doReleaseShared方法里面。doReleaseShared方法里面会根据头节点的状态来判断是唤醒后继节点还是设置节点状态为PROPAGATE。

下面来看一下早期在共享模式下获取和释放同步状态会引发的问题,下面的代码可以引发这一Bug:

import java.util.concurrent.Semaphore;

public class TestSemaphore {

   private static Semaphore sem = new Semaphore(0);

   private static class Thread1 extends Thread {
       @Override
       public void run() {
           sem.acquireUninterruptibly();
       }
   }

   private static class Thread2 extends Thread {
       @Override
       public void run() {
           sem.release();
       }
   }

   public static void main(String[] args) throws InterruptedException {
       for (int i = 0; i < 10000000; i++) {
           Thread t1 = new Thread1();
           Thread t2 = new Thread1();
           Thread t3 = new Thread2();
           Thread t4 = new Thread2();
           t1.start();
           t2.start();
           t3.start();
           t4.start();
           t1.join();
           t2.join();
           t3.join();
           t4.join();
           System.out.println(i);
       }
   }
}

上面的两个线程类 Thread1和Thread2 的作用主要是一个用来获取信号量,一个用来释放信号量。

上面的代码,在早期setHeadAndPropagate与releaseShared方法的源码中,我们在多线程的情况下可能会遇见一种bug。下面我们来看一下:
我们假设某次循环中队列里排队的节点为情况为:

信号量释放的顺序为线程t3先释放,线程t4后释放,

时刻1: 线程t3执行了releaseShared方法,在releaseShared方法内部调用了unparkSuccessor(h),唤醒了后继节点(即唤醒了线程t1),并且head的等待状态从-1变为0

时刻2: 线程t1被线程t3唤醒,调用Semaphore.NonfairSync的tryAcquireShared,在Semaphore重写的tryAcquireShared方法中返回值是当前线程获取信号量后信号量的剩余数,信号量初始为0,在t3调用了releaseShared后信号量为1,因此线程t1调用tryAcquireShared方法返回值为0,表示当前线程t1获取到信号量后信号量剩余值为0。

时刻3: 线程t4就在时刻2后调用releaseShared,这一时刻线程t1还没有执行到setHeadAndPropagate方法,因此此时线程t4中的head节点就是时刻1时的头结点,所以此时h.waitStatus为0,不满足条件,因此不会调用unparkSuccessor(h)。并且此时头结点还没有设置为线程t1所在的节点,因此就算调用了unparkSuccessor(h),也无法唤醒线程t2所在的节点。

时刻4: 这一时刻就是时刻2里面线程t1在获取到信号量后,而且在时刻3之后,继续执行的部分,当线程t1执行到调用setHeadAndPropagate时,时刻2信号量的剩余数r=0作为setHeadAndPropagate的参数传入,即propagate=0,所以在setHeadAndPropagate方法内部因为不满足propagate > 0,从而不会唤醒后继节点,即线程t2所在的节点。

因为 线程t2无法退出阻塞的状态,所以 t2.join 会阻塞主线程,导致程序挂住。

后面引入PROPAGATE状态,并且修改了setHeadAndPropagate与releaseShared方法,增加了doReleaseShared方法,解决这种问题。根据上面的bug,我们看一下引入PROPAGATE状态值和修改了源码之后的效果:
再看上面的那种情况:
时刻1:与之前一样
时刻2:与之前一样
时刻3:而在线程t4调用releaseShared后,会调用doReleaseShared方法,在doReleaseShared方法内部,此时h.waitStatus为0(此时读到的head和时刻1中为同一个head),因此将等待状态置为PROPAGATE
时刻4:线程t1调用setHeadAndPropagate时,在由于时刻3的作用,此时h.waitStatus为PROPAGATE(-3),因此h.waitStatus < 0,从而接下来调用doReleaseShared唤醒线程t2

理解一下,在PROPAGATE引入之前,在多线程的环境下,共享模式中可能会出现线程hang住的情况,可能会有队列中处于等待状态的节点因为头节点head唤醒了后继节点node1,但后继节点node1包含的线程t1还没通过setHeadAndPropagate方法设置好head,此时又有其他的线程释放锁,但是节点node1的后继节点node2包含的线程t2此时读到head状态是旧的头结点的状态为0,导致释放但不唤醒,最终后一个等待线程既没有被释放线程唤醒,也没有被持锁线程唤醒。

参考博文:

https://cloud.tencent.com/developer/article/1113761      

https://www.cnblogs.com/micrari/p/6937995.html

 

展开阅读全文

没有更多推荐了,返回首页