『深入学习Java』(七) AQS - Lock 是如何实现 Synchronized 功能的?

200 阅读5分钟

前言

上一小节学习了 synchronized 的原理,Java 语言是从 JVM 层面来处理 synchronized 关键字的。

但是 Java 语言中还有另外一种锁,即 Lock。那么 Lock 又是如何来保证多线程同步的呢?

Lock

Lock 锁的基本使用方式,我们在前面已经学习过了。

Lock 是依靠 AQS 来实现加锁解锁的,我们以最常用的 ReentrantLock 为例。

lock() 方法内部调用了Sync#lock() ,而Sync 又继承了AQS

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    public void lock() {
        sync.lock();
    }
    // 同步器
    abstract static class Sync extends AbstractQueuedSynchronizer {
        abstract void lock();
        final boolean nonfairTryAcquire(int acquires) {
            // 上锁
            // ...
        }
        protected final boolean tryRelease(int releases) {
            // 解锁
            // ...
        }
    }
    ......
}

那么 AQS 又有什么魔力,可以达到 synchronized 效果,并且更胜一筹呢?

AQS

AQS 全称是 AbstractQueuedSynchronizer,一个依赖于先进先出 (FIFO) 等待队列的阻塞锁和同步器(信号量、事件等)的框架。

AQS 提供一个被 volatile 修饰的 int 类型数据,来表示同步状态:private volatile int state; ,至于 state 的值代表什么含义,由 AQS 的子类定义。

volatile 的含义是使其他工作内存,可以“实时看见”字段值的变化。

子类维护 state 状态时,需要使用父类的getStatesetStatecompareAndSetState 的三个原子方法。

同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义锁使用。

另外,同步器支持独占式、共享式获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

等待队列

AQS 的等待队列是一个 FIFO 队列,也叫做 CLH 队列。类似于一个双向链表,也是使用一个 Node 当作节点。

    static final class Node {
        // ...
        //当前节点等待状态
        // 1:线程被取消。-1:表示释放锁时,需要唤醒 next 节点。
        // -2:表示节点在等待 Condition 唤醒。-3:表示共享同步状态,向下传播唤醒 Node。
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        // 线程
        volatile Thread thread;
         // ...
    }

image

head 就是获取同步状态成功的节点,即获取锁成功的节点。后续有新的线程竞争同步状态,会向队列尾部添加 Node 节点。

当有线程释放同步状态时,会唤醒 head.next() 尝试进行锁竞争。

独占式竞争同步状态

AQS 把竞争同步状态的方法留待给子类实现,但是 AQS 实现了竞争同步状态失败时,插入队列中的逻辑。

public final void acquire(int arg) {
    // 如果竞争失败,就把线程加入等待队列,设置线程中断。
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
​
// 获取同步状态
protected boolean tryAcquire(int arg) {
   // 这里是个空实现,留待子类实现。
}
​
// 向等待队列中,添加一个节点。
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // 添加节点到队尾
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        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) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            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();
            // 如果 node 的前一个节点是头节点,再次尝试获取同步状态
            if (p == head && tryAcquire(arg)) {
                // 如果获取成功了,就把本节点设置为头节点。
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 然后返回中断状态为false,即本线程不需要中断。
                return interrupted;
            }
            // 在这里判断了尝试获取同步状态失败后,是否应该 park。
            // 检查并更新未能获取的节点的状态。如果线程应该阻塞,则返回 true。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

tryAcquire() 的实现,我们就找最常用的ReentrantLock#Sync 来看。

// 非公平锁
static final class NonfairSync extends Sync {
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}
​
abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;
    abstract void lock();
    // 非公平获取同步状态,acquires==1。
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 如果c==0, 说明同步状态还没线程获取到,可以尝试获取,直接尝试 cas 设置同步状态。
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                // 设置同步状态成功,设置持有者线程。
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果当前线程为持有者线程,那说明是锁重入
        else if (current == getExclusiveOwnerThread()) {
            // 锁重入 state + 1
            int nextc = c + acquires;
            if (nextc < 0) throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 否则就是同步状态已经被别人获取了,返回获取失败。
        return false;
    }
}

独占式释放同步状态

// 释放同步状态
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒后续节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}
​
// 尝试释放同步状态
protected boolean tryRelease(int arg) {
  // ... 留给子类实现,稍后我们以 ReentrantLock 为例子。
}
​
private void unparkSuccessor(Node node) {
    
    int ws = node.waitStatus;
    // ws < 0,即线程需要被唤醒。
    // 通过 cas 修改 node 状态为0.
    if (ws < 0) compareAndSetWaitStatus(node, ws, 0);
​
    // 获取下一个节点
    Node s = node.next;
    // waitStatus > 0,只有一种情况就是1:线程被取消。
    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;
    }
    // 找到节点后,采用 unpark 的方式唤醒线程。
    if (s != null)
        LockSupport.unpark(s.thread);
}

我们再以 ReentrantLock#Sync 为例,查看 tryRelease 的实现。

abstract static class Sync extends AbstractQueuedSynchronizer {
    // ...
    // 独占式 releases 固定为 1。
    protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 如果当前线程不是持有同步状态的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否释放了?默认为false
    boolean free = false;
    // 只有当 c == 0 时,才释放。
    // 这是因为有重入锁的存在,我们在看加锁,即获取同步状态的方法中看到,state 会从 0 开始累计。
    // 那么解锁就一样,一直减到0,才算释放成功了。
    if (c == 0) {
        free = true;
        // 把同步状态拥有者线程设置空。
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
    // ...
}

小结

本篇主要探究了 Lock 是如何实现 Synchronized 功能,主要是依赖了 AQS 同步器。

主要分析了等待列队、独占式加锁、独占式解锁的过程与代码实现。

但是 AQS 的功能远不止于此,AQS 还可以用来做共享式锁,例如读写锁,我们后续再做学习分析。

参考资料

  • 《Java 并发编程的艺术》by 方腾飞
  • 《深入浅出 Java 多线程》by redspider 社区