AQS解析

AbstractQueuedSynchronizer (以下简写 AQS )这个抽象类是用来构建锁或者其他同步组件的基础框架, 使用一个 int 成员变量来表示同步状态, 通过内置的 FIFO 同步队列来控制获取共享资源的线程.

1. Lock接口介绍

Lock 接口实现类提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作, 此实现允许更灵活的结构, 可以具有差别很大的属性, 可以支持多个相关的 Condition (Condition 用于实现线程的通知/与唤醒机制)对象.

锁是用于控制多线程访问共享资源的工具. 通常, 锁提供对共享资源的独占访问: 一次只有一个线程可以获取锁, 对共享资源的所有访问都需要首先获取锁. 但是, 一些锁可以允许同时访问共享资源, 例如 ReadWriteLock. 虽然使用关键字 synchronized 修饰的方法或代码块, 会使得在监视器模式 (ObjectMonitor) 下编程变得非常容易(通过 synchronized 块或者方法所提供的隐式获取释放锁的便捷性). 虽然这种方式简化了锁的管理, 但是某些情况下, 还是建议采用 Lock 接口(及其相关子类)提供的显示的锁的获取和释放.

例如, 针对一个场景, 手把手进行锁获取和释放, 先获得锁 A, 然后再获取锁 B, 当锁 B 获得后, 释放锁 A 同时获取锁 C, 当锁 C 获得后, 再释放 B 同时获取锁 D, 以此类推. 这种场景下, synchronized 关键字就不那么容易实现了, 而 Lock 接口的实现类允许锁在不同的作用范围内获取和释放, 并允许以任何顺序获取和释放多个锁.

public interface Lock {
    // 获取锁, 如果当前锁不可用, 当前线程将会处于阻塞状态, 直到获取锁.
    void lock();
    // 可中断的获取锁, 与lock方法不同的是该方法可以响应中断.
    void lockInterruptibly() throws InterruptedException;
    // 尝试非阻塞方式获取锁, 调用该方法后立即返回是否成功获取锁.
    boolean tryLock();
    // 在一定的时间内尝试获取锁. 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 释放锁
    void unlock();
    // 获取等待通知组件, 该组件与当前锁进行绑定.
    // 当前线程只有拥有了锁, 才能调用组件的wait方法. 当调用wait方法后, 当前线程也将释放锁
    Condition newCondition();
}

从 Lock 类的方法中可以看到与 synchronized 的区别:

  1. 尝试非阻塞地获取锁: 当线程尝试获取锁, 如果这一时刻锁没有被其他线程获取到, 则成功获取并持有锁.
  2. 能被中断的获取锁: 与 synchronized 不同, 获取到锁的线程能够响应中断, 当获取到锁的线程被中断时, 中断异常会被抛出, 同时锁也会被释放.
  3. 超时获取锁: 在指定的截止时间之前获取锁, 如果截止时间到了任然无法获取到锁, 则返回.

Lock 类的使用方式也比较简单, 以下举例 ReentrantLock 的使用方式:

private ReentrantLock reentrantLock = new ReentrantLock();
public void method() {
    this.reentrantLock.lock();
    try {
        ......
    } finally {
        this.reentrantLock.unlock();
    }
}

查看 Lock 的实现类, 可以发现, AbstractQueuedSynchronizer (AQS) 是实现的关键. 从代码上看, Lock 接口是面向使用者的, 它定义了使用者与锁交互的接口, 隐藏了实现细节. AQS 与 Condition 才是真正的实现, 它简化了锁的实现方式, 屏蔽了同步状态的管理、线程排队、等待与唤醒等底层操作.

2. AQS 简介与代码分析

2.1 AQS 简介

AQS 被设计为大多数同步组件的基类, 这些同步组件都依赖于单个原子值 (int) 来控制同步状态, 子类必须要定义获取获取同步与释放状态的方法, 在AQS 中提供了三种方法 getState()、 setState(int newState) 及 compareAndSetState(int expect, int update) 来进行操作.

AQS 自身没有实现任何同步接口, 它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用, 同步器既可以支持独占式地获取同步状态, 也可以支持共享式地获取同步状态, 这样就可以方便实现不同类型的同步组件 (ReentrantLock、ReentrantReadWriteLock 和 CountDownLatch 等).

AQS 的设计是基于模板方法模式的, 也就是说, 使用者需要继承同步器并重写指定的方法, 随后将同步器组合在自定义同步组件的实现中, 并调用同步器提供的模板方法, 而这些模板方法将会调用使用者重写的方法. 先看下 AQS 中有哪些属性以及部分方法:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements Serializable {
    private transient volatile Node head; // 头结点
    private transient volatile Node tail; // 尾节点

    // 这个是最重要的, 代表当前锁的状态, 0代表没有被占用, 大于 0 代表有线程持有当前锁
    // 这个值可以大于 1, 是因为锁可以重入, 每次重入都加上 1
    private volatile int state;

    // 该属性继承自AbstractOwnableSynchronizer
    // 代表当前持有独占锁的线程, 举个最重要的使用例子, 因为锁可以重入, 所以每次用这个来判断当前线程是否已经拥有了锁
    private transient Thread exclusiveOwnerThread;
    
    // 独占式尝试获取同步状态, 通过CAS操作设置同步状态
    protected boolean tryAcquire(int arg) {}
    
    // 独占式获取同步状态, 如果当前线程获取同步状态成功, 则返回, 否则进入同步队列等待, 该方法会调用tryAcquire(int arg)方法. 
    public final void acquire(int arg) {}
}

在 AQS 中主要通过一个 FIFO 来控制线程的同步. 那么在实际程序中, AQS 会将获取同步状态的线程构造成一个 Node 节点, 并将该节点加入到队列中. 如果该线程获取同步状态失败会阻塞该线程, 当同步状态释放时, 会把头节点中的线程唤醒, 使其尝试获取同步状态.

static final class Node {
    static final Node SHARED = new Node(); // 标识节点当前在共享模式下
    static final Node EXCLUSIVE = null; // 标识节点当前在独占模式下
    
    volatile int waitStatus; 
    // ======== 下面的几个int常量是给waitStatus用的 ===========
    // 因为超时或者中断, Node被设置为取消状态, 被取消的Node不应该去竞争锁, 只能保持取消状态不变, 不能转换为其他状态
    static final int CANCELLED =  1; 
    // 当前一个节点的线程如果释放或者取消了同步状态, 将会将当前一个节点状态设置为SIGNAL, 用于通知下一个节点准备获取同步状态
    static final int SIGNAL    = -1; 
    // 表示这个Node在Condition的等待队列中, 其他线程调用Condition的singal方法后, 该节点会从等待队列转移到AQS的同步队列中, 等待获取同步锁
    static final int CONDITION = -2;
    // 与共享式获取同步状态有关, 表示锁的下一次获取可以无条件传播
    static final int PROPAGATE = -3; 

    volatile Node prev; // 上一个节点
    volatile Node next; // 下一个节点

    volatile Thread thread; // 当前一个节点对应的线程, 表示等待锁的线程
    Node nextWaiter; // 当前一个节点在Condition中等待队列上的下一个节点
    ...
}

在 AQS 中的同步队列, 分别有两个指针(对象), 一个 head 指向队列的头节点, 一个 tail 指向队列的尾节点.

当一个线程成功获取了同步状态(或者锁), 其他线程无法获取到同步状态, 这个时候会将该线程构造成 Node 节点, 并加入到同步队列中, 而这个加入队列的过程必须要确保线程安全, 所以在 AQS 中提供了一个基于 CAS 的设置尾节点的方法: compareAndSetTail(Node expect, Node update), 它需要传递当前线程“认为”的尾节点和当前一个节点, 只有设置成功后, 当前一个节点才正式与之前的尾节点建立关联.

在 AQS 中的同步队列中, 头节点是获取同步状态成功的节点, 头节点的线程会在释放同步状态时, 唤醒其下一个节点, 而下一个节点会在获取同步状态成功时将自己设置为头节点.

AQS同步队列

2.2 AQS 代码分析

ReentrantLock 在内部用了内部类 Sync 来管理锁, 所以真正的获取锁和释放锁是由 AQS 的实现类 Sync 来控制的. Sync 有两个实现, 分别为 NonfairSync (非公平锁)和 FairSync (公平锁):

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
    }
    static final class NonfairSync extends Sync {
        ...
    }
    static final class FairSync extends Sync{
        ...
    }
}

以下分析 ReentrantLock 中的 NonfairSync (非公平锁)的加锁实现部分, 可以从 NonfairSync 的 lock 方法开始跟踪代码. 以下是 Lock 加锁过程中的实现代码:

static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1)) // 尝试修改state
            setExclusiveOwnerThread(Thread.currentThread()); // 如果能成功上锁, 将持有锁的线程设定为当前线程
        else
            acquire(1); // 尝试获取锁, 直接查看AQS的acquire方法
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
abstract static class Sync extends AbstractQueuedSynchronizer {
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 因为是非公平锁, 直接尝试获取锁. 如果能直接获取到锁, 设定当前持有锁的线程. 
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current); // 获取到锁之后, 标记下锁被当前线程获取了.
                return true;
            }
        } else if (current == getExclusiveOwnerThread()) { 
            // 进入这个分支说明锁重入了, 需要将state + 1
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 代码运行到此处, 说明没有获取到锁. 将进入acquireQueued方法中.
        return false;
    }
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements Serializable {
    public final void acquire(int arg) { // 此时, arg == 1
        // 首先调用tryAcquire(子类中实现)方法尝试获取锁, 因为有可能直接就成功了, 不需要进队列排队.
        // 如果无法获取锁, 这时候就需要调用acquireQueued方法把线程挂起, 放入阻塞队列中.
        // 如果没有线程获取过锁, 那么tryAcquire方法直接返回true, acquire方法也就结束了.
        // 否则acquireQueued方法会将线程加到队列中的队尾
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 
            selfInterrupt();
    }
    // 该方法的作用是将线程包装成Node对象, 同时加入到队列中.
    private Node addWaiter(Node mode) { // 此时参数mode为Node.EXCLUSIVE, 代表独占模式.
        Node node = new Node(Thread.currentThread(), mode);
        // 以下几行代码是把当前Node加到链表的最后面去, 也就是进到阻塞队列的最后.
        Node pred = tail; // 尾节点, 第一次入队时, head和tail都未初始化.
        if (pred != null) {
            node.prev = pred; // 记录当前一个节点的上一个节点为原队列的尾节点
            if (compareAndSetTail(pred, node)) { // 用CAS把自己设置为尾节点. 如果设置成功, 这个节点就是阻塞队列的末尾节点
                pred.next = node; // 设置原尾节点的下一节点为当前一个节点
                return node;
            }
        }
        enq(node); // 运行到此处说明: 队尾为空(队列为空)或者CAS设置尾节点失败
        return node; // 返回当前一个节点, 进入acquireQueued方法
    }
    // 采用自旋(死循环)方式入队: 在CAS设置尾节点的过程中, 如果CAS操作返回失败, 那么就尝试CAS操作直到成功为止.
    private Node enq(final Node node) {
        for (;;) { // 只有入队成功才会跳出循环
            Node t = tail; // 尾节点
            if (t == null) { // 队列为空的情况下
                // 需要注意的是此处调用compareAndSetHead方法时传入的参数.
                if (compareAndSetHead(new Node())) // 此时的head应该为null, 此处用CAS方式设定head
                    // 注意此处设定head之后并没有直接return, 还是要继续执行for循环, 进入else分支才会中断循环
                    tail = head; // 此处设定队尾(指向了同一个Node).
            } else { // 在这个else分支中, 主要是设定当前一个节点的前后节点和使用CAS设定尾节点
                node.prev = t; // 设定当前一个节点的上一个节点是尾节点.
                if (compareAndSetTail(t, node)) { // 在一个无限循环中, 将当前一个节点设定为队尾, 一直到设定成功为止.
                    // 此时CAS设定队尾已经成功, tail已经是本方法的参数node, head和tail已经不是相同的node了. 
                    t.next = node; // 此时设定原队尾的下一个节点为当前一个节点
                    return t; // 此处返回的是原队尾, 不是node. 
                }
            }
        }
    }
    // 该方法是整个多线程竞争同步状态的关键方法.
    final boolean acquireQueued(final Node node, int arg) { // node是当前线程对应的节点
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor(); // 该方法获取当前一个节点的上一个节点.
                // 如果node是队列中的第一个节点(除了head), 那么先尝试获取锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node); // 如果获取到锁, 将当前一个节点设定为head, 调用该方法会删除node的thread和上一个节点内容.
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 运行到此处说明当前node不是头(除去head)或者获取锁没抢赢别的线程, 判断线程是否需要阻塞.
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally { // 线程被中断或者其他未知异常而进入到finally中
            if (failed) // 被中断的情况下进入if分支, 将线程从同步队列中移除, 同时唤醒下一个节点
                cancelAcquire(node);
        }
    }
    // 进入该方法说明线程没有获取到锁, 根据该方法的返回值判断是否需要挂起当前线程
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 此处的ws是前一个节点的状态
        if (ws == Node.SIGNAL) // waitStatus == -1, 说明前一个节点状态正常, 当前线程需要阻塞.
            return true;
        if (ws > 0) { // 大于0表示前一个节点取消了排队. 会被直接提出队列中.
            // 此处需要注意一点: 进入阻塞队列排队的线程会被挂起, 而唤醒线程的操作则是由前一个节点完成的.
            // 此处代码的意思是: 将当前一个节点的前一个节点指向一个waitStatus <= 0的节点, 到时候依靠它去唤醒自己.
            do { 
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node; 
        } else {
            // 代码运行到这边时, 都没有看到显示的设定过waitStatus, 所以每个node默认的waitStatus都是0.
            // 而运行到这个分支, waitStatus取值只能是0, -2, -3. 此处将前一个节点的waitStatus设置为Node.SIGNAL.
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 本方法到这边返回false, 继续执行acquireQueued方法的for循环, 然后再次进入本方法, 运行到第一个if并返回true.
        return false; 
    }
    // 这个方法很简单, 因为shouldParkAfterFailedAcquire方法返回true, 所以需要阻塞线程, 这个方法就是负责阻塞线程的.
    // 这里用了LockSupport.park(this)来挂起线程, 然后就停在这里了, 等待被唤醒.
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 线程阻塞
        return Thread.interrupted();
    }
    // 进入该方法说明需要将线程从同步队列中移除.
    private void cancelAcquire(Node node) {
        if (node == null) 
            return;
        node.thread = null; // 1. 将节点对应的线程设置为null
        Node pred = node.prev;
        while (pred.waitStatus > 0) // 2. 跳过当前节点之前已经取消的节点
            node.prev = pred = pred.prev;
        Node predNext = pred.next;
        node.waitStatus = Node.CANCELLED; // 3. 当前线程状态设置为CANCELLED(1)

        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null); // 4. 如果当前中断的节点是尾节点, 那么将尾节点重新指向上一个节点
        } else {
            int ws; // 5. 判断当前节点的上一个节点的状态, 如果是SINGAL或者设定为SINGAL成功, 那么移除当前节点
            if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL ||
                (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node); // 6. 将该节点移除, 同时唤醒下一个节点
            }
            node.next = node; // help GC
        }
    }   
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0) // 重置该节点为初始状态
            compareAndSetWaitStatus(node, ws, 0);
        // 下面的代码就是唤醒后续的节点, 但是有可能后续节点取消了等待状态.
        Node s = node.next;
        // 判断下一节点的状态, 如果为Node.CANCELED状态, 则向前遍历, 获取最近的waitStatus <= 0的节点
        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) //如果当前节点的下一个节点不为null, 则唤醒下一个节点中的线程. 
            LockSupport.unpark(s.thread);
    }
}
public class LockSupport {
    private static final sun.misc.Unsafe UNSAFE;
    private static final long parkBlockerOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            parkBlockerOffset = UNSAFE.objectFieldOffset(Thread.class.getDeclaredField("parkBlocker"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 阻塞当前线程, 如果调用unpark方法或者当前线程被中断, 才能从park方法中返回.
    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L); // 当线程阻塞时, 会阻塞在这边. 详细代码可以查看JVM的代码
        setBlocker(t, null);
    }
    private static void setBlocker(Thread t, Object arg) {
        UNSAFE.putObject(t, parkBlockerOffset, arg); // 将当前线程中parkBlocker参数设定为参数arg
    }
}

以下分析 ReentrantLock 中的 NonfairSync (非公平锁)的解锁实现部分, 可以从 NonfairSync 的 unlock 方法开始跟踪代码. 以下是 Lock 解锁过程中的实现代码:

public class ReentrantLock implements Lock, java.io.Serializable {
    public void unlock() { // 此时unlock方法只有一个线程会进入. 其他线程都未拿到锁, 正在阻塞中.
        sync.release(1); // 解锁, 此处调用AQS中的release方法
    }
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements Serializable {
    public final boolean release(int arg) { 
        if (tryRelease(arg)) { // 此处调用Sync中的tryRelease方法
            Node h = head; // 如果已经完全释放锁了, 进入这个判断中, 唤醒等待的线程.
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); // 查看加锁部分的代码解释
            return true;
        }
        return false;
    }
}
abstract static class Sync extends AbstractQueuedSynchronizer {
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false; // 是否已经完全释放锁.
        if (c == 0) { // 锁重入的问题
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
}

3. ReentrantLock 加锁

假设线程1调用了 ReentrantLock 的 lock 方法, 那么线程1将会独占锁, 整个调用链十分简单:

Markdown

线程1在调用 tryAcquire(1) 后直接返回 true, 内部先设定了 AQS 的 state 为 1, 之后设置 AQS 的 thread 为当前线程(此时并没有初始化 head 和 tail), 这两个操作完成后就表示线程1独占了锁.

之后线程2也要尝试获取同一个锁, 在线程1没有释放锁的情况下必然无法获取, 所以线程2被阻塞, 整个调用链比较复杂:

Markdown

线程2先调用 lock 方法, 尝试修改 state, 当然这一步是必定失败的, 因此线程2直接调用 acquire 方法. 在 acquire 方法中, 首先调用 tryAcquire 方法尝试获取锁, 如果无法获取到锁, 继续走 acquireQueued 方法添加至 FIFO 等待队列中.

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

首先执行 AQS 的 addWaiter 方法, 如果队列为空的情况下, 初始化 head (直接调用 new Node()), 然后将当前线程封装成 Node 并加入到等待队列中. 之后调用 acquireQueued方法.

// 该方法是整个多线程竞争同步状态的关键方法.
final boolean acquireQueued(final Node node, int arg) { // node是当前线程对应的节点
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 该方法获取当前一个节点的上一个节点.
            // 如果node是队列中的第一个节点(除了head), 那么先尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 如果获取到锁, 将当前一个节点设定为head, 调用该方法会删除node的thread和上一个节点内容.
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 运行到此处说明当前node不是头(除去head)或者获取锁没抢赢别的线程, 判断线程是否需要阻塞.
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally { // 线程被中断或者其他未知异常而进入到finally中
        if (failed) // 被中断的情况下进入if分支, 将线程从同步队列中移除, 同时唤醒下一个节点
            cancelAcquire(node);
    }
}

通过自旋的方式来获取同步状态,如果当前节点 (node1) 的上一节点是 head 且该节点获取同步状态成功,那么会设置 head 指向该节点 ,同时将上一节点的 next 指向断开。

Markdown

Markdown

此时由于线程2对应的 node 是等待队列中的第一个 Node (除去 head), 所以此时再尝试获取锁(可能线程1已经执行完成并释放了锁). 如果无法获取锁, 调用 shouldParkAfterFailedAcquire 方法.

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

线程2进入 shouldParkAfterFailedAcquire 方法后, waitStatus 的状态是默认值 0, 所以此时直接将 waitStatus 设置为 Node.SIGNAL 即 -1 并返回 false.

分析 shouldParkAfterFailedAcquire 方法的返回值, 如果返回 true: 说明前一个节点的 waitStatus 是 -1, 是正常情况, 线程需要被挂起等待, 之后需要被唤醒. 那么如果返回 false: 说明不需要被挂起, 此时就会再次进入 acquireQueued 方法中的 for 循环, 然后会再次进入 shouldParkAfterFailedAcquire 方法.

仔细看 shouldParkAfterFailedAcquire 方法可以发现, 第一次进入该方法时, 必定返回 false. 原因很简单, 前一个节点的 waitStatus 的状态是依赖后续节点设置的 (shouldParkAfterFailedAcquire 方法可以结合解锁部分的代码一起阅读). 最后调用 parkAndCheckInterrupt 方法挂起线程.

  1. 此时线程2入队, 然后调用 shouldParkAfterFailedAcquire 方法将 head 的 waitStatus 设定为-1, 而线程2的 waitStatus 并未设置, 还是为 0.
  2. 此时如果线程3再入队, 插入到线程2的后面, 此时线程3的 waitStatus 为 0, 当线程3调用 shouldParkAfterFailedAcquire 方法后, 会将线程2的 waitStatus 设置为-1.

如果线程中断, 在 acquireQueued 方法中最终会执行 finally 语句中的代码, 通过 cancelAcquire 方法将线程从同步队列中移除.

private void cancelAcquire(Node node) {
    if (node == null) 
        return;
    node.thread = null; // 1. 将节点对应的线程设置为null
    Node pred = node.prev;
    while (pred.waitStatus > 0) // 2. 跳过当前节点之前已经取消的节点
        node.prev = pred = pred.prev;
    Node predNext = pred.next;
    node.waitStatus = Node.CANCELLED; // 3. 当前线程状态设置为CANCELLED(1)

    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null); // 4. 如果当前中断的节点是尾节点, 那么将尾节点重新指向上一个节点
    } else {
        int ws; // 5. 判断当前节点的上一个节点的状态, 如果是SINGAL或者设定为SINGAL成功, 那么移除当前节点
        if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL ||
            (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node); // 6. 将该节点移除, 同时唤醒下一个节点
        }
        node.next = node; // help GC
    }
}
  1. 将节点对应的线程设置为 null
  2. 跳过当前节点之前已经取消的节点(我们已经知道在 Node.waitStatus 的枚举中,只有 CANCELLED 大于0 )

Markdown

  1. 当前线程状态设置为 CANCELLED(1)

  2. 如果当前中断的节点是尾节点, 那么则将尾节点重新指向上一个节点

Markdown

  1. 判断当前节点的上一个节点的状态, 如果是 SINGAL 或者设定为 SINGAL 成功, 那么移除当前节点

  2. 如果5的条件不满足, 那么调用 unparkSuccessor 方法移除节点, 同时唤醒下一个节点

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0) // 重置该节点为初始状态
        compareAndSetWaitStatus(node, ws, 0);
    // 下面的代码就是唤醒后续的节点, 但是有可能后续节点取消了等待状态.
    Node s = node.next;
    // 判断下一节点的状态, 如果为Node.CANCELED状态, 则向前遍历, 获取最近的waitStatus <= 0的节点
    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) //如果当前节点的下一个节点不为null, 则唤醒下一个节点中的线程. 
        LockSupport.unpark(s.thread);
}
Markdown

4. ReentrantLock 解锁

解锁的流程比较简单. 首先调用 ReentrantLock 的 unlock 方法, 内部直接调用 AQS 的 release 方法.

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

先调用 Sync 的 tryRelease 尝试释放锁, 只有当 c == 0 的时候才会真正释放锁, 这和加锁时一个线程多次调用 lock 方法累加 state 是对应的, 调用了多少次的 lock() 方法自然必须调用同样次数的 unlock() 方法才行, 这样才把一个锁给全部解开.

当线程对 ReentrantLock 全部解锁之后, AQS 的 state 自然就是 0 了, 方法返回 true. 代码运行到 unparkSuccessor 方法中, 进行唤醒线程已经移除当前节点与唤醒下一个节点.

5. 结尾

在并发状态下, 解锁和解锁需要三个部件的协调:

  1. 锁状态: 如果需要知道锁是不是被别的线程占有了, 需要判断 state. 当为 0 时表示没有线程占有该锁, 所有线程都可以争抢这个锁. 当设定为 1, 说明这个锁已经被某个线程抢到了, 其他线程只能排队等待. 如果锁重入的话, state 进行累加即可. 当需要解锁的时候, state 进行递减直至为 0. 当 state 为 0 时表示线程已经完全释放锁了, 然后唤醒等待队列中的第一个线程.
  2. 线程的阻塞和唤醒: AQS 中使用 LockSupport 类进行线程的阻塞和唤醒.
  3. 阻塞队列: 因为可以有多个线程去争抢锁, 但只能有一个线程持有锁, 其他线程需要等待. 此时需要一个队列来管理等待的线程. AQS 中维护一个 FIFO 的链表, 每个 node 都持有后续节点的引用.

posted on 2021-02-18 14:31  annwyn  阅读(149)  评论(0)    收藏  举报

导航