Java并发编程--锁原理之抽象同步队列AQS

1.AQS–锁的底层支持

AbstractQueuedSynchronizer抽象同步队列简称AQS,是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的,其类图结构如下

在这里插入图片描述

AQS是一个FIFO的双向队列,队列元素的类型为Node,其结构如下

变量 功能
SHARED 用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的
EXCLUSIVE 用来标记该线程是获取独占资源时被挂起后放入AQS队列的
CANCELLED 1,线程被取消
SIGNAL -1,线程需要被唤醒
CONDITION -2,线程在条件队列中等待
PROPAGATE -3,释放共享资源时需要通知其他节点
waitStatus 记录当前线程等待状态
prev 记录当前节点的前驱节点(阻塞队列)
next 记录当前节点的后继节点(阻塞队列)
thread 存放进入AQS队列里面的线程
nextWaiter 指向下一个节点(条件队列)

​  AQS的阻塞队列是以双向的链表的形式保存的,是通过prev和next建立起关系的,但是AQS中的条件队列是以单向链表的形式保存的,是通过nextWaiter建立起关系的,也就是AQS的阻塞队列和AQS中的条件队列并非同一个队列

​  AQS中维持了一个单一的状态信息state,可以通过CAS操作修改其值,并且它有get()和set()方法.

​  对于AQS来说,线程同步的关键对状态值state进行操作,分为独占方式(标记,如果标记状态不正确不能进行操作)和共享方式(直接进行CAS修改,不需要判断标记).

(1). 独占不响应中断模式下,获取与释放资源

1). 获取锁的过程

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

private Node addWaiter(Node mode) {
    //基于当前线程,节点类型(Node.EXCLUSIVE)创建新的节点
    //由于这里是独占模式,因此节点类型就是Node.EXCLUSIVE
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    //这里为了提搞性能,首先执行一次快速入队操作,即直接尝试将新节点加入队尾
    if (pred != null) {
        node.prev = pred;
        //这里根据CAS的逻辑,即使并发操作也只能有一个线程成功并返回,其余的都要执行后面的入队操作。即enq()方法
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        //如果队列还没有初始化,则进行初始化,即创建一个空的头节点
        if (t == null) { 
            //同样是CAS,只有一个线程可以初始化头结点成功,其余的都要重复执行循环体
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //新创建的节点指向队列尾节点,毫无疑问并发情况下这里会有多个新创建的节点指向队列尾节点
            node.prev = t;
            //基于这一步的CAS,不管前一步有多少新节点都指向了尾节点,这一步只有一个能真正入队成功,其他的都必须重新执行循环体
            if (compareAndSetTail(t, node)) {
                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);
                p.next = null; //帮助GC
                //表示锁资源成功获取,因此把failed置为false
                failed = false;
                //返回中断标记,表示当前节点是被正常唤醒还是被中断唤醒
                return interrupted;
            }
            //如果没有获取锁成功,则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //最后会分析获取锁失败处理
        if (failed)
            cancelAcquire(node);
    }
}

//node是当前线程的节点,pred是它的前置节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前置节点的waitStatus
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        //如果前置节点的waitStatus是Node.SIGNAL则返回true,然后会执行parkAndCheckInterrupt()方法进行挂起
        return true;
    if (ws > 0) {
        //由waitStatus的几个取值可以判断这里表示前置节点被取消
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        //这里我们由当前节点的前置节点开始,一直向前找最近的一个没有被取消的节点
        //注,由于头结点head是通过new Node()创建,它的waitStatus为0,因此这里不会出现空指针问题,也就是说最多就是找到头节点上面的循环就退出了
        pred.next = noparkAndCheckInterrupt()de;
    } else {
        //根据waitStatus的取值限定,这里waitStatus的值只能是0或者PROPAGATE,那么我们把前置节点的waitStatus设为Node.SIGNAL然后重新进入该方法进行判断
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  1. 当前线程调用acquire()申请获取锁资源
    1. 先调用tryAcquire()尝试获取锁(这个方法内部由具体的锁实现)
    2. 如果失败先调用addWaiter()将当前线程入队到AQS队列中
      1. 如果之前没有创建队列(队尾为null),直接调用enq进行完整的入队操作(包括初始化)
        1. 是一个自旋循环,第一次循环创建一个空结点,设置为队尾和队首,第二次循环时将传入的node入队
      2. 如果已经有队尾存在了,先将node(待插入结点)的前置设置为tail,使用t指针指向tail,使用CAS将tail指针指向node,然后将实际上的队尾t的next设置为node.
    3. 然后在acquireQueued()方法中等待被唤醒直到获取到资源.
      1. 进入acquireQueued方法后会进入一个无限循环.每一次循环都会判断传入的参数是不是队首,并且尝试一次锁获取
      2. 如果获取成功,将自己设置为队首,并将node.thread设置为null,保证头结点永远是一个不带thread的空节点.
      3. 如果获取失败,调用shouldParkAfterFailedAcquire()判断自己需不需要阻塞.阻塞的大前提是前继节点是可被唤醒的(waitStatus设置为Signal),这样才能让自己有机会被唤醒(队列中按顺序唤醒)
        1. 判断传入的前继节点的waitStatus是否为Signal,是的话直接返回ture
        2. 根据waitStatus的值分为两种情况
        3. 如果waitStatus大于0:也就是前继节点被取消了.一直向前查找一个没有被取消的节点(waitStatus>=0)的结点(头结点waitStatus为0或-1,因此不可能出现空指针),并拼接成双向队列(后面的队列都是waitStatus>=0的,在状态表中只有被取消的线程是这个状态,所以被移除出队列,下一次GC会被清除),返回false
        4. 如果waitStatus不大于0,也就是为0,-2或-3:修改前继节点waitStatus为-1
      4. 如果shouldParkAfterFailedAcquire()返回true,证明node的前继节点可以被唤醒,立即调用parkAndCheckInterrupt()阻塞挂起node
      5. 如果shouldParkAfterFailedAcquire()返回false,证明在方法内部找到了一个可以被唤醒的节点作为前继节点.在下一次循环中进行挂起
      6. parkAndCheckInterrupt()方法的挂起并不会被中断打断,是非中断挂起,如果挂起过程中出现了中断,方法在返回时会返回true,否则返回false
      7. 如果上一步返回了true,证明出现了中断,在下一次循环中将中断标记返回到上一层代码.
    4. 线程被唤醒之后会将返回中断标记到这里,如果出现过中断,将自身线程挂起,来弥补之前的中断.
  2. 进入临界区(线程中运行的用户代码)

2). 锁的释放过程

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

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        //把标记为设置为0,表示唤醒操作已经开始进行,提高并发环境下性能
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    //如果当前节点的后继节点为null,或者已经被取消
    if (s == null || s.waitStatus > 0) {
        s = null;
        //注意这个循环没有break,也就是说它是从后往前找,一直找到离当前节点最近的一个等待唤醒的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //执行唤醒操作
    if (s != null)
        LockSupport.unpark(s.thread);
}
  1. 调用tryRelease()来释放资源.(判断当前线程是否和AQS中的线程一致,然后对state进行修改)(这里返回值是指是否已经完全释放资源了,可重入锁需要将state释放至0)
  2. 拿到head节点,判断其状态,如果不为null,且waitStatus不为0(初始状态下,为0,当这个节点有了后继节点时,会被修改成-1,这里是为了防止只有一个头节点的空队列进行了释放锁),调用unparkSuccessor()唤醒头结点的后继节点的线程
    1. 先进行waitStatus的判断,如果waitStatus<0,将其设置为0,避免其他线程也进入此方法,提高高并发环境下的性能.
    2. 找到头结点的后继节点,再次判断是否为空或者其状态值是否被取消
    3. 如果后继节点为空或者状态为被取消,从后向前找到最前面的一个等待唤醒的节点
    4. 执行唤醒操作
  3. 返回true

 如果唤醒的节点前有被取消的节点,那么会在唤醒后被判断前继节点不是头结点,再次进行判断前继节点是否能被唤醒的方法调用.在这个方法调用中会将前面的被取消的节点移除,然后再次进行前继节点是不是头结点的判断.这次就可以判断成功,将此节点设置为头结点,成功唤醒,运行其内部用户代码.

(2). 共享不响应中断模式下,获取与释放资源

1). 获取锁的过程

public final void acquireShared(int arg) {
    //尝试获取共享锁,返回值小于0表示获取失败
    if (tryAcquireShared(arg) < 0)
        //执行获取锁失败以后的方法
        doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
    //添加等待节点的方法跟独占锁一样,唯一区别就是节点类型变为了共享型
    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);
                //注意上面说的,等于0表示不用唤醒后继节点,大于0需要
                if (r >= 0) {
                    //获取到锁以后的唤醒操作
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    //如果是因为中断醒来则设置中断标记位
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //挂起逻辑跟独占锁一样
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        //获取失败的取消逻辑跟独占锁一样
        if (failed)
            cancelAcquire(node);
    }
}

//两个入参,一个是当前成功获取共享锁的节点,一个就是tryAcquireShared方法的返回值,注意上面说的,它可能大于0也可能等于0
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; //记录当前头节点
    //设置新的头节点,即把当前获取到锁的节点设置为头节点
    //注:这里是获取到锁之后的操作,不需要并发控制
    setHead(node);
    //这里意思有两种情况是需要执行唤醒操作
    //1.propagate > 0 表示调用方指明了后继节点需要被唤醒
    //2.头节点后面的节点需要被唤醒(waitStatus<0),不论是老的头结点还是新的头结点
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //如果当前节点的后继节点是共享类型或者没有后继节点,则进行唤醒
        //这里可以理解为除非明确指明不需要唤醒(后继等待节点是独占类型),否则都要唤醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
  1. 调用acquireShared()去获取锁
    1. 首先会尝试调用tryAcquireShared()共享方式获取资源,返回值负数表示失败,0表示成功,但没有剩余资源可用,整数表示成功且有剩余资源
    2. 当返回负数时,调用doAcquireShared()进行线程的入队列挂起等待
      1. 将当前线程打包成Node对象,类型为共享型,添加到队列尾,方法与独占式相同
      2. 如果当前节点的前继节点是头结点,尝试进行获取资源,如果返回值不为负,意味着可以获取到资源,调用setHeadAndPropagate()将当前节点设置为头结点,判断是否是因为唤醒线程来到这里,对中断标记进行判断,是否进行中断补充
        1. setHeadAndPropagate(Node node, int propagate)的第二个参数是刚刚尝试获取资源得到的剩余资源数,如果大于零,代表有多余资源,那么应该去唤醒下一个可被唤醒的线程
        2. h == null 场景未知,应该不会出现吧.
        3. h.waitStatus意味着头结点为可唤醒状态
        4. 以上的集中状况都需要进行线程唤醒的尝试.如果当前头结点的前继节点不为空且为共享型,调用doReleaseShared()对头结点后的第一个可唤醒节点进行唤醒.
      3. 如果前继节点不是头节点,挂起,逻辑同独占锁

2). 释放锁的过程

public final boolean releaseShared(int arg) {
    //尝试释放共享锁
    if (tryReleaseShared(arg)) {
        //唤醒过程
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        //唤醒操作由头结点开始,注意这里的头节点已经是上面新设置的头结点了
        //其实就是唤醒上面新获取到共享锁的节点的后继节点
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //表示后继节点需要被唤醒
            if (ws == Node.SIGNAL) {
                //这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;      
                //执行唤醒操作      
                unparkSuccessor(h);
            }
            //如果后继节点暂时不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }unparkSuccessor
        //如果头结点没有发生变化,表示设置完成,退出循环
        //如果头结点发生变化,比如说其他线程获取到了锁,为了使自己的唤醒动作可以传递,必须进行重试
        if (h == head)                   
            break;
    }
}
  1. 调用releaseShared()获取资源
    1. 先调用tryReleaseShared()尝试释放共享锁,由具体的锁实现逻辑,如果获取到了资源,返回true.继而调用doReleaseShared()唤醒头结点后的可唤醒线程
      1. 进入一个自旋循环
      2. 判断头结点(如果从设置头结点调用至此方法,此时头结点是新的头节点)是否为空,是否只有一个头结点组成空队列.
      3. 如果都不是,那么头结点有效,然后判断头结点状态,如果为-1,设置为0,
      4. 调用unparkSuccessor()执行唤醒操作(别的线程在21行会判断为true,跳过,这样就控制了并发)
        1. unparkSuccessor()与独占锁相同,如果头结点的后继节点不能唤醒,从后向前找到最前面的一个等待唤醒的节点唤醒它
      5. 然后跳入27行将状态改为共享模式下可释放,适应在设置头结点处的调用.
      6. 如果头结点没有变化(并发情况下,其他线程可能会改变head的引用),退出方法.

2. AQS–条件变量的支持

​  和之前讲到的notify()和wait()配合synchronized内置锁实现线程间同步一样,AQS中的条件变量signal()和await()方法也是用来配合锁(使用AQS实现)来实现线程间同步的.

​  不同点在于,synchronized同时之能与一个共享变量的notify()或wait()方法实现同步,而AQS的一个锁可以对应多个变量.

(1). 条件队列阻塞的过程(功能类比wait()方法)

public final void await() throws InterruptedException {
    //如果当前线程被中断则直接抛出异常
    if (Thread.interrupted())
        throw new InterruptedException();
    //把当前节点加入条件队列
    Node node = addConditionWaiter();
    //释放掉已经获取的独占锁资源
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //如果不在同步队列中则不断挂起
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        //中断处理,另一种跳出循环的方式
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //走到这里说明节点已经条件满足被加入到了同步队列中或者中断了
    //跟独占锁调用同样的获取锁方法,从这里可以看出条件队列只能用于独占锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //到这里说明已经成功获取到了独占锁,接下来就做些收尾工作
    //删除条件队列中被取消的节点
    if (node.nextWaiter != null) 
        unlinkCancelledWaiters();
    //根据不同模式处理中断
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

//注:1.与同步队列不同,条件队列头尾指针是firstWaiter跟lastWaiter
//注:2.条件队列是在获取锁之后,也就是临界区进行操作,因此很多地方不用考虑并发
private Node addConditionWaiter() {
    Node t = lastWaiter;
    //如果最后一个节点被取消,则删除队列中被取消的节点
    //至于为啥是最后一个节点后面会分析
    if (t != null && t.waitStatus != Node.CONDITION) {
        //删除所有被取消的节点
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //创建一个类型为CONDITION的节点并加入队列,由于在临界区,所以这里不用并发控制
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

//删除取消节点,就是链表删除
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

//入参就是新创建的节点,即当前节点
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        //获取当前的state并释放,这从另一个角度说明必须是独占锁
        int savedState = getState();
        //跟独占锁释放锁资源一样
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            //如果这里释放失败,则抛出异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        //如果释放锁失败,则把节点取消,由这里就能看出来上面添加节点的逻辑中只需要判断最后一个节点是否被取消就可以了
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

//判断节点是否在同步队列中
final boolean isOnSyncQueue(Node node) {
    //判断1:节点状态或者节点没有前置节点
    //注:同步队列是有头节点的,而条件队列没有
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    //判断2:next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段
    if (node.next != null) 
        return true;
    //上面如果无法判断则进入复杂判断
    return findNodeFromTail(node);
}

//注意这里用的是tail,这是因为条件队列中的节点是被加入到同步队列尾部,这样查找更快
//从同步队列尾节点开始向前查找当前节点,如果找到则说明在,否则不在
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

//这里的判断逻辑是:
//1.如果现在不是中断的,即正常被signal唤醒则返回0
//2.如果节点由中断加入同步队列则返回THROW_IE,由signal加入同步队列则返回REINTERRUPT
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}

//修改节点状态并加入同步队列
//该方法返回true表示节点由中断加入同步队列,返回false表示由signal加入同步队列
final boolean transferAfterCancelledWait(Node node) {
    //这里设置节点状态为0,如果成功则加入同步队列
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        //与独占锁同样的加入队列逻辑,不赘述
        enq(node);
        return true;
    }
    //如果上面设置失败,说明节点已经被signal唤醒,由于signal操作会将节点加入同步队列,只需自旋等待即可
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

//根据中断时机选择抛出异常或者设置线程中断状态
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
      if (interruptMode == THROW_IE)
           throw new InterruptedException();
      else if (interruptMode == REINTERRUPT)
           //实现代码为:Thread.currentThread().interrupt();
           selfInterrupt();
}
  1. 调用await()方法将当前线程移入条件队列,等待条件队列唤醒
    1. 如果当前线程被中断,抛出异常
    2. 调用addConditionWaiter()将当前线程加入条件队列
      1. 如果最后一个节点被取消了,调用unlinkCancelledWaiters()清理队列中的被取消节点
        1. 该方法的逻辑就是从头遍历队列,如果状态为被取消,删除该节点
      2. 创建一个类型为条件等待的节点加入队列尾
      3. 返回该节点
    3. 调用fullyRelease()释放掉当前线程占用的资源,这一步之前的操作都是线程安全的
      1. 获取当前的state值,调用release()释放资源(内部逻辑之前有讲),如果成功则返回并标记为成功,失败抛出异常,
      2. 最后如果没有成功,也就是释放锁失败了,将节点取消(从这里可以看出来,被取消的节点一定是结尾处的,所以上面addConditionWaiter()方法判断的是最后一个节点是否被取消,这样保证了队列中间不可能出现被取消节点)
    4. 循环调用isOnSyncQueue()判断当前线程是否在同步队列中(如果不在则相当于内置锁中被wait()阻塞但没有被notify()唤醒的状态),这里是调用通过其他线程调用signal()方法将该线程(条件)唤醒
      1. 先进行快速判断,如果该节点状态为条件等待(肯定在条件队列中),或者前继节点(同步队列中)为空,证明不在同步队列中
      2. 如果后继节点(同步队列中)不为空,则证明在同步队列中,因为只有在队列中的节点才有可能被添加后继节点
      3. 如果上面两个都不能判断,调用findNodeFromTail()进入复杂判断
        1. 从尾节点向前查找节点,如果找到了当前节点,证明当前节点在同步队列中
    5. 如果不在同步队列中,进行挂起.并进行中断处理
    6. 然后在acquireQueued()方法中等待被唤醒直到获取到资源.(内部逻辑之前有描述过)
    7. 如果,当前node不是尾节点,说明其他线程也对条件队列进行了操作,调用unlinkCancelledWaiters()清理队列中被取消的节点
    8. 根据不同模式,判断是否调用reportInterruptAfterWait()处理中断
      1. 根据中断时机选择抛出异常或者中断补偿(取决于能否响应中断)

(2). 条件队列唤醒的过程(功能类比notify()方法)

public final void signal() {
    //如果不是独占锁则抛出异常,再次说明条件队列只适用于独占锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //如果条件队列不为空,则进行唤醒操作
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

//该方法就是把一个有效节点从条件队列中删除并加入同步队列
//如果失败则会查找条件队列上等待的下一个节点直到队列为空
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) {
    //修改节点状态,这里如果修改失败只有一种可能就是该节点被取消,具体看上面await过程分析
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    //跟独占锁入队方法一样
    Node p = enq(node);
    //注:这里的p节点是当前节点的前置节点
    int ws = p.waitStatus;
    //如果前置节点被取消或者修改状态失败则直接唤醒当前节点
    //此时当前节点已经处于同步队列中,唤醒会进行锁获取或者正确的挂起操作
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}
  1. 调用signal()条件唤醒条件队列中的一个线程,将该线程从条件队列中移动到同步队列中
    1. 如果不是独占锁,抛出异常,条件队列只能用于独占锁
    2. 如果条件队列不为空,调用doSignal()执行唤醒操作
      1. 从传入结点(上一步传入的是头结点)开始把一个有效节点从条件队列删除
      2. 这一步使用first保存头结点,然后头结点后移(循环会判断头结点为空退出循环)
      3. 调用transferForSignal()将刚刚得到的first节点移入同步队列(成功则退出循环)
        1. 使用AQS修改该节点的状态,将CONDITION(条件等待)修改为0(如果修改失败肯定是线程被取消了或者其他线程在这一步状态为刚刚修改的0,因为条件队列中的线程状态只能是CONDITION或者被取消,这一步修改的不算)如果失败,返回false
        2. 调用enq()将此节点入同步队列,并返回前继节点(逻辑不再赘述)
        3. 如果前继节点被取消或者修改状态失败,挂起当前节点,唤醒后进入await()中的acquireQueued()部分自旋挂起等待唤醒(这个唤醒是锁的竞争,而不是条件控制的).
  2. 然后会回到上层方法调用,循环判断当前线程是否在同步队列中
发布了141 篇原创文章 · 获赞 47 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_41596568/article/details/104075953