专栏原创出处: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(); // 解锁 } } }
Copied!
在 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; } }
Copied!
# 非公平锁-加锁分析
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); }
Copied!
# 公平锁非公平锁-解锁分析
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; }
Copied!
释放锁的最后写 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 状态失败怎么办?
# 参考
- 并发编程的艺术