专栏原创出处:github-源笔记文件 (opens new window) ,github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。
Java 并发编程专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java 并发编程 (opens new window)
# 什么是锁
随着集成电路越来越发达,多计算核心的机器大行其道,为了解决多个并行执行分支对某一块资源的同步访问,操作系统层面提供了 互斥信号量 的概念。 在几乎所有的支持多线程编程模型的语言中,基本上都提供了与互斥信号量对应的概念,在 Java 中我们称之为锁。
# 锁的内存语义分析
本文将借助 ReentrantLock 的源代码,来分析锁内存语义的具体实现机制。
ReentrantLock 是可重入锁,即可多次加锁。涉及 volatile、CAS 参考《内存模型-volatile》《内存模型-CAS实现原理》
public class ReentrantLockExample {
int value = 0;
private final ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock(); // 加锁
try {
value++;
} finally {
lock.unlock(); // 解锁
}
}
public void reader() {
lock.lock(); // 加锁
try {
int tmp = value;
// do something ...
System.out.println(tmp);
} finally {
lock.unlock(); // 解锁
}
}
}
在 ReentrantLock 中,调用 lock() 方法获取锁;调用 unlock() 方法释放锁。
ReentrantLock 的实现依赖于 Java 同步器框架 AbstractQueuedSynchronizer(本文简称之为 AQS)。
AQS 使用一个整型的 volatile 变量 (命名为 state) 来维护同步状态。
ReentrantLock-类图关系
ReentrantLock 内部实现了公平锁(FairSync)和非公平锁(NonfairSync)。
公平锁和非公平锁加锁逻辑存在差异,公平锁按线程排队优先级获取锁,非公平自然竞争。解锁逻辑完全一样。
# 公平锁-加锁分析
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
// 公平锁-加锁最终执行方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取锁的开始,首先读 volatile 变量 state
if (c == 0) { // 可以竞争
if (!hasQueuedPredecessors() && // 是否排在线程队列头节点(公平)
compareAndSetState(0, acquires)) { // CAS 方式修改 state
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 当前线程重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
# 非公平锁-加锁分析
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) // 无竞争情况直接获取锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 存在竞争使用 nonfairTryAcquire 竞争锁资源
}
// nonfairTryAcquire 实现中无需判断 hasQueuedPredecessors 线程优先级
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// AbstractQueuedSynchronizer 中提供 CAS 操作
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
# 公平锁非公平锁-解锁分析
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); // 释放锁的最后,写 volatile 变量 state
return free;
}
释放锁的最后写 volatile 变量 state,在获取锁时首先读这个 volatile 变量。
根据 volatile 的 happens-before 规则,释放锁的线程在写 volatile 变量之前可见的共享变量,在获取锁的线程读取同一个 volatile 变量后将立即变得对获取锁的线程可见。
# 锁内存语义总结
由于 Java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了下面 4 种方式。
- A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
- A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
- A 线程用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
- A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。
锁的通用化的实现模式:
- 声明共享变量为 volatile。
- 使用 CAS 的原子条件更新来实现线程之间的同步。
- 配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。
# concurrent 包实现
volatile 变量的读/写和 CAS 可以实现线程之间的通信。把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。
AQS,非阻塞数据结构和原子变量类 (java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。
concurrent 包实现层级 |
---|
Lock、同步器、阻塞队列、Executor、并发容器 |
↑ ↑ |
AQS、非阻塞数据结构、原子变量类 |
↑ ↑ |
volatile 变量的读写、CAS |
# 思考
- 为什么锁需要 [volatile 变量的读写、CAS] 来提供内存语义,仅使用 volatile 可以吗?
- 如果加锁时 CAS 修改 state 状态失败怎么办?
# 参考
- 并发编程的艺术