Java 锁相关详解 【一、锁的基础概念】
一、锁的基础概念(专业加强版)
目标:从 硬件 → JMM → 语言/库语义 三层逐步压实认识,厘清“为什么需要锁”“锁到底保障了什么”“不用锁如何正确”。
1.1 并发的硬件背景与风险来源
1.1.1 多级缓存与一致性
- 现代 CPU 以 L1/L2/L3 多级缓存 + 写缓冲(write buffer) 提升吞吐,导致线程可能读到过期值。
- 缓存一致性协议(如 MESI) 只保证单一地址的读写可见性收敛,不保证跨地址的顺序(这正是“有序性”问题产生的根源)。
- Cache Line(常见 64B) 是一致性与伪共享的粒度:不同线程写入同一行内不同变量,也会相互抖动(false sharing)。
1.1.2 指令重排与内存次序
- 编译器、JIT、CPU 都可进行 as-if-serial 层面的重排(在单线程下不改变结果的前提)。
- 内存模型差异:x86 接近 TSO(较强),ARM/POWER 等更弱;这影响屏障需求。
- 直观结论:不加同步,你无法要求“先写 A 再写 B,另一个线程一定按 A→B 的顺序看到”。
1.2 Java 内存模型(JMM)核心
JMM 通过一组“同步动作(synchronization actions)”和“happens-before(HB) 规则”定义“何为正确的并发可见性与顺序”。
1.2.1 同步动作(不完全列举)
monitorenter/monitorexit(synchronized)volatile读/写- 线程生命周期:
Thread.start()、Thread.join()、interrupt() - 对象构造的
final字段发布语义 - 显式栅栏(
VarHandle.fullFence/acquireFence/releaseFence等)
1.2.2 happens-before(HB)规则(关键子集)
- 程序次序规则:同一线程内,程序顺序上的前操作 HB 后操作。
- 监视器锁规则:对同一把锁的解锁 HB 其后对同一把锁的加锁。
- volatile 规则:对某个
volatile变量的写 HB 随后对该变量的读。 - 线程启动规则:
Thread.start()HB 被启动线程中的第一个动作。 - 线程终止/等待规则:线程中的所有动作 HB 成功的
Thread.join()返回。 - 中断规则:对线程
interrupt()HB 该线程检测到中断(如isInterrupted()返回 true)。 - 传递性:HB 具有传递性(A→B 且 B→C 则 A→C)。
SC-for-DRF:若程序无数据竞争(Data-Race-Free),JMM 保证其表现为顺序一致性。因此正确同步(建立 HB)是根本。
1.2.3 原子性、可见性、有序性在 JMM 下的落点
- 原子性:
volatile的单次读写是原子的(long/double 在 JDK5+ 也原子);复合操作需要锁或 CAS。 - 可见性:
synchronized解锁与volatile写会把变更刷新至主内存,后续加锁/读取方看见。 - 有序性:
volatile写具有 release 语义,volatile读具有 acquire 语义;锁解/加同理提供 释放/获取 语义。
1.3 Java 语言与库级同步语义对照
| 原语/机制 | 可见性 | 有序性 | 原子性 | 进阶特性 | 典型用途 |
|---|---|---|---|---|---|
synchronized | ✅ | ✅(锁释放/获取的内存语义) | ✅(临界区互斥) | 可重入、JIT 优化(消除/粗化) | 严格互斥、简单正确 |
volatile | ✅ | ✅(W:release / R:acquire) | ❌(仅单次读写) | 低开销标志位、DCL 指针可见性 | 状态标志、安全发布引用 |
J.U.C Lock(如 ReentrantLock) | ✅ | ✅(与 synchronized 等价) | ✅ | 可中断、公平性、tryLock | 可控性强的互斥 |
ReadWriteLock / StampedLock | ✅ | ✅ | 部分(读共享/写独占) | 乐观读/降级、读多写少优化 | 高读场景 |
原子类(Atomic*) | ✅ | ✅(CAS 有 acquire/release 语义) | 复合操作借助 CAS | 无锁化、性能好 | 计数器、指针交换 |
VarHandle / Fences | ✅ | ✅(显式 fence 精细控制) | 依实现 | 近底层、可移植 | 构建并发原语 |
说明:实际的“屏障”是编译器/JIT/CPU 的实现细节。JMM 只承诺语义,JIT 在不同 CPU 上插入合适屏障(如 x86
MFENCE仅用于 Store→Load)。
1.4 可见性与有序性陷阱:双重检查锁(DCL)
1.4.1 错误示例(缺 volatile)
class Singleton {
private static Singleton INSTANCE; // 缺 volatile!
private Singleton() {}
static Singleton getInstance() {
if (INSTANCE == null) { // 1. 读
synchronized (Singleton.class) {
if (INSTANCE == null) { // 2. 再读
INSTANCE = new Singleton(); // 3. 赋值
}
}
}
return INSTANCE;
}
}
问题:对象创建可被重排为:分配内存 → 将引用赋给 INSTANCE → 调用构造函数。
其他线程可能在构造未完成前读到非空引用(半初始化)。
1.4.2 正确示例(加 volatile)
class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
volatile 保障写入具有 release 语义,读具有 acquire 语义,禁止关键重排,确保安全发布。
1.5 安全发布(Safe Publication)与对象不变性
1.5.1 四种可靠发布方式
- 静态初始化:类初始化阶段天然同步。
- 经由
volatile引用发布:volatile写建立发布 HB。 - 经由
final字段:构造函数内不泄漏 this,final字段对读取方有额外可见性保障(初始化安全性)。 - 经由恰当同步的容器:比如放入
ConcurrentHashMap、BlockingQueue之后再被其他线程获取。
1.5.2 反例:构造期间泄漏 this
class Bad {
static List<Bad> holder = new ArrayList<>();
final int x;
Bad() {
holder.add(this); // 泄漏 this
x = 42; // final 字段,可能对读者不可见为 42
}
}
在构造过程完成前把 this 暴露给其他线程,会破坏 final 的初始化保障。
1.6 “锁”与“无锁”的进度保证(Liveness/Progress)
1.6.1 进度属性
-
安全性(Safety):永不发生坏事(如不同时进入临界区)。
-
活性(Liveness):好事最终发生(不会永远卡住)。
-
无锁进度级别:
- Wait-freedom:任何线程在有限步内完成。
- Lock-freedom:系统整体持续前进(个别线程可能饿死)。
- Obstruction-freedom:无竞争时有限步内完成。
1.6.2 CAS 与 ABA
- CAS 是无锁算法基石,存在 ABA 问题:值从 A→B→A,CAS 误判为未变。
- 方案:版本戳(
AtomicStampedReference)、指针分离、/重用避免、或更高层协议。
1.7 屏障与硬件模型的直觉映射
| JMM 层语义 | 常见实现(x86/ARM 粗略直觉) | 说明 |
|---|---|---|
| 释放(Release) | 写写有序;必要时 StoreStore/StoreLoad | 使先前写对他线程可见 |
| 获取(Acquire) | 读后不越过;必要时 LoadLoad/LoadStore | 禁止后续读写越过该读 |
| 全栅栏(Full Fence) | MFENCE/DMB SY | 阻断一切重排(保守) |
注:具体由 HotSpot C2/JIT 选择最小必要指令;我们在应用层只需依赖语义而非具体指令。
1.8 性能度量与基准测试方法论
1.8.1 微基准常见坑
- 死代码消除/DCE、逃逸分析导致测试代码被优化掉。
- JIT 暖身(warmup)不足导致结果抖动。
- 缓存副作用、伪共享干扰吞吐。
1.8.2 建议:使用 JMH
@State(Scope.Group)
public class CounterBench {
private final AtomicLong a = new AtomicLong();
private long b;
@Group("atomic")
@Benchmark
@GroupThreads(4)
public void atomicInc() {
a.incrementAndGet();
}
@Group("plain")
@Benchmark
@GroupThreads(4)
public void plainInc() { // 非线程安全,仅作对照
b++;
}
}
- Warmup/Measurement 与 Fork 合理设置;观察 p99 延迟 与 吞吐,而非单一平均值。
1.9 以图析理:JMM 的 HB 关系与发布路径

1.10 工程实践清单(Checklist)
- 是否存在数据竞争:所有共享可变状态要么在锁保护下访问,要么经由
volatile/安全发布。 - 是否正确发布:对象构造完成后再暴露;必要时使用
volatile引用或并发容器传递。 - 是否需要互斥:复合读改写选择
synchronized/Lock或使用原子类封装逻辑。 - 是否读多写少:考虑
ReadWriteLock/StampedLock(注意写偏与饥饿)。 - 是否有伪共享:高写热点结构考虑填充/分离字段(如
@Contended,需-XX:-RestrictContended)。 - 是否可替代为无锁结构:队列/计数器等优先尝试原子类与现成并发容器。
- 压测与实证:用 JMH / 生产压测验证假设,结合火焰图与事件记录(JFR)。
1.11 关键结论(TL;DR)
- HB 是一切并发正确性的根:不用 HB(锁/volatile/线程边界等)就无权要求他线程“看见”或“按顺序”。
volatile≠ 锁:它提供可见性与(受限的)有序性,但不提供互斥,无法保障复合操作安全。- 安全发布优先:发布路径决定对象可见性的上限——错误发布会让所有后续同步失效。
- 度量先于优化:不要依靠直觉优化并发,先用工具实证,再选择锁/无锁/读写锁等策略。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120830

浙公网安备 33010602011771号