JUC并发编程基石——AQS深入解读

深入解读AQS源码

前言

   Java.util.concurrent 是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如ReentrantLock,Semaphore,CountDownLatch、ThreadPoolExecutor等等。并发包的作者是大名鼎鼎的 Doug Lea。
  AQS是一个用来构建锁和同步器的框架,全名AbstractQueuedSynchronizer,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如上面提到的ReentrantLock,Semaphore,ThreadPoolExecutor其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。要了解AQS我们先要了解jdk提供的两大核心底层技术——CAS以及LockSupport工具类。

文章目录

  • 深入解读AQS源码
    • 前言
    • 什么是CAS
    • LockSupport工具类
    • 独占模式
      • 获取锁
      • 释放锁
    • 共享模式
      • 获取共享锁
      • 释放共享锁
    • 线程间通信
      • 等待
      • 通知

什么是CAS

   CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。 CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
   java提供了Unsafe类来支持CAS操作,Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。 这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。看一下Unsafe反编译出来的CAS的代码



    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

var1为int字段所在的对象,var2为int字段的偏移量,通过这两个值可以定位到int字段所在的内存空间,var4为原来的值,var5为期望更新的值。通过循环的方式比较原来的值,并更新成期望的值直到成功。除了提供CAS操作之外,Unsafe还提供了对象操作、线程调度、系统信息获取、内存屏障、等相关API。下面讲的LockSupport类支持的线程调度也是基于Unsafe类实现的。

LockSupport工具类

   AQS中的线程调度是通过LockSupport工具类实现的,主要依赖这几个方法

//挂起当前线程
public static void park() {
       //第二个参数是超时时间,0的话表示无限阻塞中
       //直到被唤醒或被中断
        UNSAFE.park(false, 0L);
    }
 //重载方法挂起当前线程并设置线程的parkBlocker字段
public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        //设置线程的parkBlocker字段
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        //线程被唤醒后需要清空parkBlocker字段
        setBlocker(t, null);
    }
    //超时时间阻塞
public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            //阻塞时间为nanos,如果没有其他线程唤醒或中断的话
            //时间到了会自动醒过来
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }

总的来说,调用了park函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行

  • 其他某个线程将当前线程作为目标调用 unpark。
  • 其他某个线程中断当前线程。
  • 该调用不合逻辑地(即毫无理由地)返回。

   再来看一下unpark函数,如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果

public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

  AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列实现的。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系

AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配,即将暂时获取不到锁的线程包装成Node加入到队列中。如果同一时间只能有一个线程获取共享资源,就叫做独占模式,一般用来实现独占锁。如果同一时间可以有多个线程同时获得锁,就叫做共享模式,一般用来实现共享锁。

独占模式

  独占模式下,只能有一个线程能获得共享资源,如果有其他线程同时去请求共享资源的话会被包装成Node并添加到CLH队列中 (在AQS源码中叫sync队列,后面的分析都叫该名字),并且该线程会被阻塞挂起。Node是定义在AQS中的一个内部类,我们先看一下它的实现

//内部类Node,sync queue和condition queue上具体的结点类型
    static final class Node {
        //共享模式,如果某个结点处于共享模式,nextWaiter指针就指向该常量字段
        static final Node SHARED = new Node();
    
        //独占模式,如果某个结点处于独占模式,nextWaiter指针就指向该常量字段
        static final Node EXCLUSIVE = null;

        //结点状态字段,表示该结点上的线程被取消
        static final int CANCELLED =  1;
        
        //结点状态字段,表示该结点的后继结点上的线程需要唤醒
        static final int SIGNAL    = -1;
       
        //结点状态字段,表示该结点处于condition 队列
        static final int CONDITION = -2;
       
        //结点状态字段,共享模式下表示下次acquireShared无条件传播
        //在doReleaseShared方法中使用,用于释放后继的共享结点
        static final int PROPAGATE = -3;

        //结点的状态,上面那几种
        volatile int waitStatus;

        //前驱结点指针,用于sync队列
        volatile Node prev;

        //后继结点指针,用于sync队列
        volatile Node next;

        //结点上的线程
        volatile Thread thread;

        //condition队列中,下一个结点的指针
        //sync队列中,表示结点的模式,共享或独占
        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;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

构建sync队列时使用了prev、next两个指针,是一个双向队列。再来看一下AQS中相关字段

    //sync队列头结点指针
    private transient volatile Node head;

    //sync队列尾结点指针
    private transient volatile Node tail;

    //同步状态字段,即共享资源,也可以理解为"锁"
    private volatile int state;

字段非常简洁,就是维护了一个sync队列以及共享资源,因为AQS是在多线程环境中使用的,所以都是volatile声明的,保证了多线程可见性。再来看一下相关字段的一些内存偏移量

private static final Unsafe unsafe = Unsafe.getUnsafe();
    //state字段的偏移量
    private static final long stateOffset;
    //head字段的偏移量
    private static final long headOffset;
    //tail字段的偏移量
    private static final long tailOffset;
    //Node结点中waitStatus字段的偏移量
    private static final long waitStatusOffset;
    //Node结点中next字段的偏移量
    private static final long nextOffset;

    static {
        try {
            //获取AbstractQueuedSynchronizer类中state字段偏移量
            stateOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
            //获取AbstractQueuedSynchronizer类中head字段偏移量
            headOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
            //获取AbstractQueuedSynchronizer类中tail字段偏移量
            tailOffset = unsafe.objectFieldOffset
                (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
            //获取Node类中waitStatus字段偏移量
            waitStatusOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("waitStatus"));
            //获取Node类中next字段偏移量
            nextOffset = unsafe.objectFieldOffset
                (Node.class.getDeclaredField("next"));

        } catch (Exception ex) { throw new Error(ex); }
    }

通过Unsafe获取了这些字段的偏移量,用于CAS更新字段的值。

获取锁

  当多个线程并发抢占共享资源的时候,只能有一个线程获得,其他失败的线程会被包装成Node排列在sync队列上

    //获取独占锁,不响应中断,如果tryAcquire失败则该线程
    //会阻塞在sync队列上,被唤醒后会再次去尝试tryAcquire直到成功为止
    //tryAcquire方法是由用户自己实现,表示获取独占锁成功
    public final void acquire(int arg) {
        //如果获取锁失败,则将该线程以独占模式包装成Node并添加到sync队列尾部
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            //如果是中断唤醒的需要将线程的中断标志复位
            selfInterrupt();
    }

AQS是一个框架,采用模板方法的设计模式实现了底层最核心的功能,用户自定义的获取独占锁逻辑则由tryAcquire实现,如果获取失败则先通过addWaiter方法添加到sync队列尾部

    //创建一个结点包含当前线程,以及该结点的模式(独占、共享)
    private Node addWaiter(Node mode) {
        //创建结点node
        Node node = new Node(Thread.currentThread(), mode);
        //该结点的前驱结点是尾结点
        Node pred = tail;
        //快速入队,不存在并发的情况下可以直接成功入队(插入sync队列尾部)
        //并发情况下,也是有机会直接入队成功的
        if (pred != null) {
            //当前结点的前驱指针先指向尾结点
            node.prev = pred;
            //cas方式插入尾结点
            //这里是先更新尾结点指针tail指向当前结点
            if (compareAndSetTail(pred, node)) {
                //cas更新tail指针成功后再更新尾结点的next指针指向当前结点
                pred.next = node;
                return node;
            }
        }
        //如果快速入队失败,则自旋加cas方式直到入队成功
        enq(node);
        return node;
    }

     private Node enq(final Node node) {
        //自旋
        for (;;) {
            //获取最新的尾结点
            Node t = tail;
            if (t == null) {
                //尾结点不存在说明sync队列是空的,创建头结点head
                //head结点是一个空结点
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //当前结点的前驱指针先指向尾结点
                node.prev = t;
                //cas更新tail指针指向当前结点
                //如果失败说明存在竞争,则继续循环直到成功
                if (compareAndSetTail(t, node)) {
                    //更新tail指针成功后再将尾结点的next指针指向当前结点
                    //入队成功
                    t.next = node;
                    return t;
                }
            }
        }
    }

入队成功以后通过acquireQueued方法会再尝试获取一次锁,如果还是失败则将前驱结点状态更新成SIGNAL,然后再尝试获取一次,如果仍旧失败的话则进入阻塞。

//独占模式下获取锁,如果获取失败就会进入阻塞状态,唤醒后会再次尝试获取锁,如果还是
    //失败的话就继续阻塞,直到最后成功(或发生异常)跳出该方法
    final boolean acquireQueued(final Node node, int arg) {
        //失败标识字段
        boolean failed = true;
        try {
            //中断标识字段
            boolean interrupted = false;
            //自旋直到获取锁成功
            for (;;) {
                //前驱结点
                final Node p = node.predecessor();
                //如果前驱结点是头结点,并且获取锁成功
                //tryAcquire方法是用户自定义的获取独占锁逻辑
                //因为只有头结点的后继结点的线程可以去尝试获取锁,所以其他结点上的线程是不会去跟它竞争锁的
                //除非有其他不在sync队列的线程也执行了tryAcquire,并且获取锁成功
                //则该结点下的线程仍会失败
                if (p == head && tryAcquire(arg)) {
                    //更新当前结点为新的头结点
                    setHead(node);
                    //原头结点指针置空
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //先判断当前线程是否可以挂起(park),挂起的先决条件是前驱结点的waitStatus更新成功SIGNAL
                //更新成功后将当前线程挂起,线程醒来后再判断一下是否执行了中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //如果是被中断唤醒的设置true
                    interrupted = true;
            }
        } finally {
            //发生了不可预期的异常
            if (failed)
                //取消当前结点的线程获取锁的资格
                cancelAcquire(node);
        }
    }

当线程被唤醒后,会先判断前驱结点是否是头结点,因为只有第一个进入队列的线程才有资格获取锁,如果获取成功了就将自己所在的结点设置成头结点。再看一下进入阻塞的方法

    //判断前驱结点的状态是否是SIGNAL,将前驱结点状态更新成SIGNAL,并清空取消的结点
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果已经是SIGNAL直接返回
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
            //大于0说明前驱结点状态是取消CANCELLED
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            //从后往前遍历,直到找到非CANCELLED结点
            //并将中间CANCELLED的结点清除掉
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 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.
             */
            //cas更新前驱结点的waitStatus字段为SIGNAL
            //如果失败则进入外层循环下次继续直到成功
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

更新前驱的状态为SIGNAL成功之后,调用LockSupport#park进入阻塞

private final boolean parkAndCheckInterrupt() {
        //挂起当前线程
        LockSupport.park(this);
        //判断是否执行了线程中断,因为线程醒来可能是其他线程执行了unpark
        //或者其他线程执行了interrupt
        //该方法还会重置中断标识,恢复到中断之前的状态
        return Thread.interrupted();
    }

如果发生了异常的话需要将当前线程的结点取消

//取消当前线程所在的结点
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        //当前结点的前驱结点
        Node pred = node.prev;
        //如果前驱结点被取消了,继续向前探测直到找到未取消的结点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        //有效前驱结点的后继结点,肯定是已被取消或即将要取消(当前结点)的结点
        Node predNext = pred.next;

        //当前结点的状态置为取消
        //这下predNext肯定是已被取消的结点
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        //如果当前结点是尾结点,并且cas将尾结点的指针tail指向前驱结点成功
        if (node == tail && compareAndSetTail(node, pred)) {
            //前驱结点(也就是新的尾结点)的后继结点置空
            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或者设置SIGNAL状态成功
            //3.前驱结点的线程不能为空
            //满足这三个条件后可以放心的把当前结点node摘除掉,而且它后面的结点也有机会被唤醒
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                //如果当前node的后继结点状态正常的话
                //则把前驱结点的后继结点替换成node的后继结点
                //相当于把取消状态的结点都摘除了
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                //如果上述三个条件不满足的话
                //则需要立即唤醒当前node结点的后继结点
                //因为node结点的状态已经设置成取消了但是结点还在队列上,还没被摘除
                //后继结点的线程被唤醒后会去尝试获取锁,如果失败的话会把它之前的已经取消的结点全部清空掉
                //然后再设置它自己的前驱结点的状态为SIGNAL,然后又进入阻塞状态
                unparkSuccessor(node);
            }
            //方便gc回收
            node.next = node; // help GC
        }
    }

上面阻塞中的线程被中断后不会抛异常,下面看一个响应中断的方法,和acquire相比就是多了一步响应中断

//独占模式下获取锁,响应中断
    public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        //首先进来就判断线程是否被中断,如果被中断的话抛中断异常
        if (Thread.interrupted())
            throw new InterruptedException();
        //如果获取锁失败
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

//和acquireQueued方法相比就是多了抛异常的操作
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                //如果parkAndCheckInterrupt方法返回true的话,说明是该线程是被其他线程
                //中断唤醒的,需要响应中断抛异常
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

除了响应中断外,AQS还提供了一个超时阻塞获取锁的方法

    //独占模式下超时等待获取锁,并且响应中断
    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }

    //独占模式下超时等待获取锁,并且响应中断
    //和doAcquireInterruptibly相比,多了超时时间的判断
    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                //已经超过超时时间了,直接返回false
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    //nanosTimeout时间内没有被唤醒的话,线程会自动恢复
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            //在这个方法里,如果nanosTimeout时间内没有获取锁的话,直接取消该结点
            if (failed)
                cancelAcquire(node);
        }
    }

释放锁

最后看一下独占模式下的锁释放,同样需要用户实现tryRelease方法释放独占锁。因为独占锁只有一个线程获得锁,所以释放的时候也肯定只有一个线程执行释放,即不会存在并发的情况

    //独占模式下的锁释放
    public final boolean release(int arg) {
        //tryRelease有实现类实现,是用户自定义的锁释放逻辑
        if (tryRelease(arg)) {
            Node h = head;
            //如果头结点的状态等于0的话说明肯定有其他获取锁失败的线程创建了头结点,刚完成头结点的初始化
            //后面的线程还没有进入阻塞状态,所以不需要唤醒
            //进入阻塞的前提是将前驱结点的状态设为SIGNAL(-1)
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    //唤醒后继结点上的线程
    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;
        //如果当前结点没有被取消的话,将它的状态设置成初始状态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.
         */
        //后继结点
        Node s = node.next;
        //如果后继结点不存在,或者状态为取消
        if (s == null || s.waitStatus > 0) {
            s = null;
            //从后往前找,找到一个离node最近的状态不为取消的结点,为什么要从后往前找呢
            //因为新结点入队的方式是这样的:假设新结点叫newNode
            //第一步,newNode的前驱指针prev指向尾结点
            //第二步,cas更新尾指针tail指向newNode
            //第三步,newNode的前驱结点的next指针指向newNode
            //如果还没走到第三步,我们就可以认为newNode结点加入了,只是还没有最终入队,这时候就有可能出现s==null
            //从后往前遍历的话就不会遗漏newNode结点了
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //如果存在状态正常的后继结点,唤醒结点上的线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

来看一下获取锁,释放锁的整个过程
JUC并发编程基石——AQS深入解读_第1张图片


共享模式

获取共享锁

  共享模式下允许多个线程同时获取共享资源,和独占模式不一样的是线程被唤醒后会设置当前线程所在的结点的为头结点,然后会继续唤醒后继所有结点上的线程,如果唤醒的线程没有获得共享锁会再次进入阻塞状态

    //共享模式下获取锁,不响应中断
    public final void acquireShared(int arg) {
        //tryAcquireShared由实现类实现,获取共享锁
        if (tryAcquireShared(arg) < 0)
            //获取共享锁失败
            doAcquireShared(arg);
    }
    
    //共享模式下获取锁,和独占锁差不多,唯一不一样的是
    //线程被唤醒后会继续唤醒后面的结点上的线程
    private void doAcquireShared(int arg) {
        //创建SHARED模式的结点,并添加到sync队列尾部
        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) {
                        //设置当前线程的结点为头结点,并继续唤醒后面的线程如果条件允许的话
                        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);
        }
    }

setHeadAndPropagate方法会设置当前结点为头结点,并继续唤醒后面的共享结点

private void setHeadAndPropagate(Node node, int propagate) {
        //记录原来的头结点
        Node h = head; // Record old head for check below
        //设置当前线程的结点为头结点
        setHead(node);

        //1.propagate > 0的话说明还有共享锁可以获取,短路后面的条件判断继续唤醒后面的线程,如果等于0的话理论上就没有共享锁了
        //h==null空指针判断,理论上是不会存在为空的情况的
        //h.waitStatus < 0,这里如果又有一个线程释放了共享锁,执行了doReleaseShared方法,但是head结点的状态已经是0了,
        //所以就把状态更新成PROPAGATE,好让当前被唤醒的线程感知到,即又有共享锁可以获取了,可以继续唤醒后面的线程
        //(h = head) == null新的头结点赋值给h变量
        //h.waitStatus < 0,这里如果存在后继结点的话肯定会小于0(后继结点阻塞前会把它更新成SIGNAL),也就是说即使没有共享锁了
        //也会去唤醒后继阻塞的线程,可能会造成不必要唤醒
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            //后继结点不为空并且是共享模式,继续唤醒
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

doReleaseShared方法在释放共享锁的方法中也有调用,因为可能有多个线程获得共享资源,所以释放锁的时候会存在多个线程同时释放,再加上被唤醒的线程也会执行doReleaseShared方法,所以doReleaseShared方法会存在并发,除了acquireShared方法获取共享锁外还存在响应中断获取、超时等待获取两种方法,这里就不介绍了。下面看一下释放共享资源的方法

释放共享锁

   //释放共享锁
    //因为共享锁可以被多个线程获取,所以释放锁也有可能被多个线程调用
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            //释放锁成功就唤醒sync队列上头结点后继的线程
            doReleaseShared();
            return true;
        }
        return false;
    }

重点看一下doReleaseShared方法,该方法没有参数,直接从头结点开始唤醒后面的线程

     //从头结点开始唤醒后继结点上的线程,这个方法有两个地方被调用
    //1.获得共享锁的线程释放共享锁
    //1.被唤醒的线程会继续唤醒后面的线程
    private void doReleaseShared() {

        for (;;) {
            Node h = head;
            //即sync队列上存在等待获取锁的结点
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //并发情况下,第一个进来的线程肯定能判断为true
                if (ws == Node.SIGNAL) {
                    //把头结点状态更新成0,然后再唤醒后继结点上的线程
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                //并发情况下,如果已经被其他线程更新成0了,就把它更新成PROPAGATE,好让已经被唤醒的线程感知到继续唤醒后面的结点
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            //如果头结点更新了,说明被唤醒的线程已经更新了头结点
            //那么当前线程就继续循环去唤醒后面的结点,但是被唤醒的线程也会去唤醒后面的结点
            //这里会存在并发调用doReleaseShared,但是有上面的cas保证,所以唤醒线程是安全的
            if (h == head)                   // loop if head changed
                break;
        }
    }

线程间通信

  在synchronized机制下,获得锁的线程可以通过wait方法释放锁并进入等待状态,另一个线程获得锁执行完任务后通过notify方法通知等待中的线程,从而实现了线程间的通信。AQS有没有提供这样的机制呢?有,通过Condition接口实现。先看一下接口中的方法

public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;


    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}

await方法以及await开头的方法表示释放当前锁并进入等待状态,signal以及signalAll方法通知等待中的线程。可以看到Condition的api比synchronized机制下更丰富。

等待

   ConditionObject类实现了Condition接口,是AQS的一个内部类,它维护了一个Condition队列,释放锁进入等待的线程都包装成Node排列在Condition队列上,当另一个线程执行了signal方法后会把Condition队列上第一个Node移到sync队列上。先来看一下await方法

        //线程等待,并且响应中断
        public final void await() throws InterruptedException {
            //判断是否发生过中断
            if (Thread.interrupted())
                throw new InterruptedException();
            //将当前线程包装成node添加到Condition队列尾部
            Node node = addConditionWaiter();
            //释放所有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            //第一次进来肯定不在sync队列上
            while (!isOnSyncQueue(node)) {
                //挂起当前线程
                LockSupport.park(this);
                //醒来后如果interruptMode!=0,说明结点已经被转移到sync队列上了
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            //转移到sync队列后尝试去获取锁,如果获取失败进入阻塞状态,醒来后再判断中断标志,REINTERRUPT说明是中断标志复位
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                //清空掉已经取消的结点
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                //判断是要抛异常,还是恢复中断标志
                reportInterruptAfterWait(interruptMode);
        }

private void unlinkCancelledWaiters() {
            Node t = firstWaiter;
            //记录前一个非取消的结点
            //因为是单向链表所以需要通过临时变量记录
            Node trail = null;
            //从头结点开始遍历
            while (t != null) {
                Node next = t.nextWaiter;
                //如果当前结点t是取消状态的话就,将它逐出队列
                if (t.waitStatus != Node.CONDITION) {
                    //指针置空
                    t.nextWaiter = null;
                    //如果不存在正常结点的话firstWaiter就指向下一个结点
                    if (trail == null)
                        firstWaiter = next;
                    else
                        //正常结点的nextWaiter指针指向当前结点的下一个结点
                        trail.nextWaiter = next;
                    //遍历到尾部了
                    if (next == null)
                        lastWaiter = trail;
                }
                else
                    //记录非取消的结点
                    trail = t;
                t = next;
            }
        }

当前执行await方法后释放所有的共享资源,并包装成Node加入Condition队列,Condition队列是一个单向链表。再看一下释放共享资源的操作

    //释放所有的锁,使state恢复到初始的状态
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            //这里释放所有的锁,前提是当前线程得拿到了锁
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                //释放锁失败抛异常
                throw new IllegalMonitorStateException();
            }
        } finally {
            //如果失败了取消当前结点
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

调用了独占模式下的release方法释放共享资源,也就是说必须拿了独占锁才有资格执行await,否则抛异常IllegalMonitorStateException。isOnSyncQueue判断是否在sync队列上

    //判断当前结点node是否在sync队列上
    final boolean isOnSyncQueue(Node node) {
        //sync队列上不会存在CONDITION状态的结点
        //sync队列上prev==null的结点只能是head结点,head结点是一个空结点,里面不包含线程
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //next指针不为空的话肯定在sync队列上,Condition队列上的prev、next指针都是空的
        if (node.next != null) // If has successor, it must be on queue
            return true;

        //这种情况下可能当前结点刚被唤醒,正在加入sync队列,还没有最终完成以下三步
        //enq方法加入sync队列分三步
        //1.更新当前结点prev指针指向前驱结点
        //2.cas更新tail指针
        //3.更新前驱结点的next指针指向当前结点
        //所以要从后往前遍历,保证第一步是完成的前提下
        return findNodeFromTail(node);
    }
	
	//从后往前查找是否存在node结点
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

如果不在sync队列上的话就挂起当前线程,当它醒来后会去判断自己是怎么醒过来的,并做相应的处理

       //判断是被中断唤醒的还是signal唤醒的或者自动醒来的
        //如果没有发生中断的话就是0
        private int checkInterruptWhileWaiting(Node node) {
            //判断是否被中断唤醒的,不是中断的话返回0
            //如果中断的话再判断是在signal执行之前还是之后
            //之前的话需要抛异常
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

    //判断当前线程是在singal方法之前被中断的还是之后中断的
    final boolean transferAfterCancelledWait(Node node) {
        //这里如果cas更新成功,说明中断发生在signal之前
        //因为signal唤醒线程之前也有这步操作
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            //需要自己将结点转移到sync队列上
            enq(node);
            return true;
        }
    
        //如果上面cas失败了,说明signal方法先执行了
        //也就是说被signal更新了0的状态
        //那么就需要等到signa方法将结点转移到sync队列
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

当被转移到sync队列上后的逻辑和加入sync队列时的逻辑一样,先尝试去获取锁,如果失败的话还是在sync队列上阻塞。当拿到锁以后还要响应中断。再来看一个await方法的变体——超时等待,唯一不一样的就是超时时间到了会自动转移到sync队列上

//线程超时等待,并且响应中断,和await()不一样的地方是不会一直阻塞
        //在nanosTimeout时间范围内等待
        public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            final long deadline = System.nanoTime() + nanosTimeout;
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {
                    //如果时间已到那么就将该结点转移到sync队列上
                    transferAfterCancelledWait(node);
                    break;
                }
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();
        }

通知

  看一下Condition的两个通知方法signal、signalAll,signal就是将Condition队列中第一个结点转移到sync队列,signalAll将所有结点转移到sync队列。

//通知信号,将Condition队列上第一个结点转移到sync 队列
        public final void signal() {
            //isHeldExclusively方法由子类实现,用于判断当前线程是否持有锁
            if (!isHeldExclusively())
                //当前线程没有锁的话抛异常,不能发起通知
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            //转移头结点
            if (first != null)
                doSignal(first);
        }

         //将first结点移到sync队列上,如果失败的话就清理掉当前结点,并将first后继结点移到sync队列上
        //直到有一个结点成功或所有结点都失败
        private void doSignal(Node first) {
            do {
                //先清空掉当前结点
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                first.nextWaiter = null;
                //如果转移失败的话就继续下一个结点
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

final boolean transferForSignal(Node node) {
  
        //先将当前结点状态设为0,表示初始化结点,因为马上要去sync队列了
        //如果失败的话可能该线程被中断唤醒了,自己已将结点移到了sync队列上了
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        //cas加自旋的方式将当前结点移到sync队列
        //返回当前结点的前驱结点
        Node p = enq(node);
        //前驱结点的状态
        int ws = p.waitStatus;
        //这时,当前结点node已经加入了sync队列
        //如果前驱结点为取消,或者更新前驱结点SIGNAL失败了就唤醒node上的线程
        //它会去尝试获取锁,如果失败就会将前面取消的结点清空掉,并且将前驱结点
        //更新成SIGNAL
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

signalAll和signal方法不一样的地方就是signalAll会将所有Condition队列上的结点转移到sync队列

        //通知信号,唤醒所有Condition队列上的结点
        public final void signalAll() {
            //isHeldExclusively方法由子类实现,用于判断当前线程是否持有锁
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                //从头结点开始唤醒所有的结点
                doSignalAll(first);
        }
 		
 		//将Condition队列上的所有结点都移到sync队列上
        private void doSignalAll(Node first) {
            //清空头尾指针
            lastWaiter = firstWaiter = null;
            do {
                //逐个遍历转移到sync队列
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

最后再用一张图来演示一下线程获取锁、等待、通知、释放锁的过程
JUC并发编程基石——AQS深入解读_第2张图片

你可能感兴趣的:(java多线程并发,多线程,java,队列,并发编程)