锁:AbstractQueuedSynchronizer源码解析(下)

1.释放锁

释放锁是在显式调用Lock.unlock()方法时触发的,目的是让线程释放对资源的访问权。unlock方法的基础方法是releasereleaseShared,分别代表排它锁和共享锁的释放。

释放排它锁—release方法

排它锁的释放过程比较简单,分为两步

  1. 首先尝试tryRelease方法释放锁,如果失败会返回 false;反之,如果成功则执行步骤2
  2. 判断同步队列中是否还有节点在等待,如果有,此时当前节点一定是同步队列的头节点。调用unparkedSuccessor方法唤醒同步队列中的一个后继节点

具体源码如下,

public final boolean release(int arg) {
    // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去arg
    // 如果返回 true 说明成功释放锁。
    if (tryRelease(arg)) {
        Node h = head;
        // 说明见下方
        if (h != null && h.waitStatus != 0)
            // 从同步队列头开始唤醒等待锁的节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

对上面的判断if (h != null && h.waitStatus != 0)进行说明,
3. 头节点为空说明同步队列尚未初始化
4. 若头节点的节点状态为0,说明是在enq方法中compareAndSetHead(new Node())初始化的头节点。这种情况可能是其他节点加入同步队列后,由于一些原因取消了,被从同步队列中清除,导致队列中只有一个waitStatus=0的节点

大部分情况下,当节点指向的线程释放锁时,该节点应该是同步队列的头节点。但是也有特殊情况,这是由于acquire方法中的操作造成的。上面两种情况都说明,同步队列是空的。这种情况下,无需进行后续节点的唤醒操作。

unparkSuccessor方法

unparkSuccessor方法是的作用是唤醒同步队列中下一个节点,

private void unparkSuccessor(Node node) {
    // node节点是当前释放锁的节点,也是同步队列的头节点
    int ws = node.waitStatus;
    // 节点未被取消了,把节点的状态置为初始化
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 拿出 node 节点的后面一个节点
    Node s = node.next;
    // 判断说明见下方
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 从同步队列尾开始迭代,具体原因见下方
        for (Node t = tail; t != null && t != node; t = t.prev)
            // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
            if (t.waitStatus <= 0)
                s = t;
    }
    // 唤醒最靠近同步队列队头的状态不为CANCELLED的节点
    if (s != null)
        LockSupport.unpark(s.thread);
}

上方代码中的判断if (s == null || s.waitStatus > 0)

  1. s 为空,表示 node 的后一个节点为空
  2. s.waitStatus 大于0,代表 s 节点已经被取消了

遇到以上这两种情况,就从队尾开始,向前遍历,找到最靠近队头的一个 waitStatus 字段不是被取消的节点对象。

寻找最靠近同步队列头部的状态不为cancelled的节点的过程是从同步队列尾部开始的,具体原因如下,

  • 主要是因为节点被阻塞的时候,是在 acquireQueued 方法的for循环中被阻塞的,唤醒时也一定会在 acquireQueued 方法的for循环里面被唤醒
  • 唤醒之后会进入新一轮循环,在循环中会判断当前节点的前置节点是否是头节点。
  • 从尾到头的迭代顺序目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。

acquireQueued方法过程说明见博文

获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程

共享锁释放—releaseShared方法

释放共享锁分为两步,

  1. 首先尝试tryReleaseShared释放当前共享锁,失败会返回 false,如果成功释放,则执行步骤2
  2. 调用doReleaseShared方法唤醒后继节点

源码如下,

// 共享模式下,释放当前线程的共享锁
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
        doReleaseShared();
        return true;
    }
    return false;
}

doReleaseShared方法

该方法跟独占式锁释放过程有点点不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态,这里采用的CAS保证,当CAS操作失败continue,在下一次循环中进行重试。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;  
        }
        if (h == head)  
            break;
    }
}

2. 条件队列重要方法

首先展示条件队列的结构,单向链表,
image
之前synchronized代码中,利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列。而并发包中的Lock拥有一个同步队列和多个条件队列。示意图如
image
ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。

进入条件队列—await方法

当调用condition.await()方法后会使得当前获取lock的线程进入到条件队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock

await方法源码如下,

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 加入到条件队列的队尾
    Node node = addConditionWaiter();
    // 完全释放所占用资源,说明见下方
    int savedState = fullyRelease(node);
    
    int interruptMode = 0;
    // 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的
    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) 
        // 如果状态不是CONDITION,就会自动删除
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

await方法的具体流程,

  1. 调用addConditionWaiter方法将当前线程包装成Node,插入到条件队列的队尾
  2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
  3. 进入 while循环,判断当前线程的节点不在同步队列中,之后阻塞。阻塞是在 while循环中发生的,所以唤醒后也是在while循环中
  4. 当节点被 signalsignalAll方法从条件队列中调回同步队列后会跳出while循环并调用 acquireQueued方法获取lock

1)addConditionWaiter方法

该方法将线程包装为节点插入到条件队列的队尾,源码如下,

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果尾部的 waiter 不是 CONDITION 状态了,删除
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 新建条件队列 node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 队列是空的,直接放到队列头
    if (t == null)
        firstWaiter = node;
    // 队列不为空,直接到队列尾部
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

该方法返回的是包装后的Node对象。

该方法中,在将节点接到队尾之前会先判断条件队列尾节点的状态,如果不是Node.CONDITION就会调用unlinkCancelledWaiters方法删除条件队列中所有状态不为Node.CONDITION的节点。

同时可以看出条件队列是一个不带头结点的链式队列,之前学习AQS时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。

2)unlinkCancelledWaiters方法

该方法从条件队列头部开始,删除掉所有状态不对的节点,源码如下,

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    // trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        // 当前node的状态不是CONDITION,删除自己
        if (t.waitStatus != Node.CONDITION) {
            //删除当前node
            t.nextWaiter = null;
            // 如果 trail 是空的,循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION
            // 都已经被删除了,所以移动队列头节点到当前节点的下一个节点
            if (trail == null)
                firstWaiter = next;
            // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上
            else
                trail.nextWaiter = next;
            // 遍历结束,最后一次找到的CONDITION节点就是尾节点
            if (next == null)
                lastWaiter = trail;
        }
        // 状态是 CONDITION 的 Node
        else
            trail = t;
        // 继续循环,循环顺序从头到尾
        t = next;
    }
}

流程示意图如下,
image

3)fullyRelease方法

当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease方法实现,源码如下,

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

调用AQS的方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。

4)从await方法中退出

while (!isOnSyncQueue(node))退出循环的条件是节点从条件队列中取出放回至同步队列。目前想到的只有两种可能,

  1. node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了
  2. 线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去

退出循环后直接尝试 acquireQueued方法在同步队列中阻塞直至获取锁。

线程唤醒

1)单个线程唤醒—signal方法

调用condition的signal或者signalAll方法可以将条件队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照条件队列是先进先出(FIFO)的,所以条件队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。

该方法对应下图的浅蓝色曲线的过程,
在这里插入图片描述

signal方法是ConditionObject类中的方法,源码如下,

public final void signal() {
	// 第一步检验
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 从头节点开始唤醒
    Node first = firstWaiter;
    if (first != null)
        // doSignal 方法会把条件队列中的节点转移到同步队列中去
        doSignal(first);
}
  1. signal方法首先会检测调用signal方法的线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点
  2. 之后的操作的doSignal方法也是基于该节点。

调用ConditionObject对象的signal方法的前提条件是当前线程已经获取了lock,该方法会使得条件队列中的头节点,即等待时间最长的那个节点移入到同步队列。移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出

条件队列头节点移入同步队列—doSignal方法

该方法源码如下,

private void doSignal(Node first) {
    do {
        // firstWaiter指向当前节点的nextWaiter
        // 若firstWaiter为空,说明到队尾了
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
            
        // 从队列头部开始唤醒
        first.nextWaiter = null;
        
        // transferForSignal 方法会把节点转移到同步队列中去
        // (first = firstWaiter) = null 说明队列中的元素已经循环完了
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}
  1. doSignal方法将条件队列头节点的后置节点置为 null,这种操作其实就是把 node 从条件队列中移除。
  2. 通过 while 保证 transferForSignal方法能执行成功

唤醒过程最关键方法—transferForSignal

transferForSignal 方法会把节点转移到同步队列中去,该方法是真正对头节点做处理的逻辑,源码如下,

// 传入参数 node 是条件队列的头节点
final boolean transferForSignal(Node node) {
    // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 当前队列加入到同步队列,返回的 p 是 node 加入同步队列后的前置节点
    Node p = enq(node);
    int ws = p.waitStatus;
    
    // 状态修改成 SIGNAL,如果成功直接返回
    // 把前置节点p的状态改成 SIGNAL 是因为 SIGNAL 本身就表示该节点后面的节点都是需要被唤醒的
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
        LockSupport.unpark(node.thread);
    return true;
}

transferForSignal方法返回 true 表示转移成功, false 表示转移失败。

2)全部线程唤醒—signalAll方法

signalAll方法的作用是唤醒条件队列中全部节点,源码如下,

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 拿到头节点
    Node first = firstWaiter;
    if (first != null)
        // 从头节点开始唤醒条件队列中所有的节点
        doSignalAll(first);
}

上方代码与signal方法的区别仅在于将内部调用的方法从doSignal改为doSignalAll

条件队列所有节点移入同步队列—doSignalAll方法

把条件队列所有节点依次转移到同步队列去,

// 传入参数 node 是条件队列的头节点
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        // 拿出条件队列队列头节点的下一个节点
        Node next = first.nextWaiter;
        // 把头节点从条件队列中删除
        first.nextWaiter = null;
        // 头节点转移到同步队列中去
        transferForSignal(first);
        // 开始循环头节点的下一个节点
        first = next;
    } while (first != null);
}

该方法将条件队列中的每一个节点都移入到同步队列中,即“通知”当前调用ConditionObject#await()方法的每一个线程

3. await与signal/signalAll的结合思考

图示

使用 ConditionObject 提供的awaitsignal/signalAll方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”。

await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现,
在这里插入图片描述

  1. 线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列
  2. 另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中
  3. 当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。

示例

public class AwaitSignal {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();
    private static volatile boolean flag = false;

    public static void main(String[] args) {
        Thread waiter = new Thread(new waiter());
        waiter.start();
        Thread signaler = new Thread(new signaler());
        signaler.start();
    }

    static class waiter implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                while (!flag) {
                    System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
            } finally {
                lock.unlock();
            }
        }
    }

    static class signaler implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                flag = true;
                condition.signalAll();
            } finally {
                lock.unlock();
            }
        }
    }
}

上面代码的执行结果,

Thread-0当前条件不满足等待
Thread-0接收到通知,条件满足

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章