前言
上一小节学习了 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 状态时,需要使用父类的getState
、 setState
和compareAndSetState
的三个原子方法。
同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义锁使用。
另外,同步器支持独占式、共享式获取同步状态,这样就可以方便实现不同类型的同步组件(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;
// ...
}
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 社区