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

·  阅读 55

前言

上一小节学习了 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 社区
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改