Java的锁机制问题
锁机制
1.锁监视器
在 Java 并发编程中,锁监视器(Monitor) 是对象内部与锁关联的同步机制,用于控制多线程对共享资源的访问。以下是核心要点:
🔒 监视器的核心组成
-
独占区(Ownership)
- 一次仅允许一个线程持有监视器(即获得锁)
- 通过
synchronized
关键字实现
-
入口区(Entry Set)
- 竞争锁的线程队列(未获得锁的线程在此等待)
-
等待区(Wait Set)
- 调用
wait()
的线程释放锁后进入此区域 - 需通过
notify()
/notifyAll()
唤醒
- 调用
⚙️ 关键操作
操作 | 作用 | 触发条件 |
---|---|---|
synchronized | 线程尝试获取监视器锁,成功则进入独占区,失败则阻塞在入口区 | 进入同步代码块/方法时 |
wait() | 释放锁并进入等待区,线程状态变为 WAITING | 必须在持有锁时调用 (synchronized 内) |
notify() | 随机唤醒一个等待区的线程(唤醒后需重新竞争锁) | 必须在持有锁时调用 |
notifyAll() | 唤醒所有等待区的线程 | 必须在持有锁时调用 |
🔄 工作流程示例
public class MonitorDemo {private final Object lock = new Object(); // 锁监视器关联到此对象public void doWork() throws InterruptedException {synchronized (lock) { // 1. 线程进入独占区while (条件不满足) {lock.wait(); // 2. 释放锁并进入等待区}// 3. 执行临界区代码lock.notifyAll(); // 4. 唤醒其他等待线程}}
}
⚠️ 重要规则
-
锁绑定对象
每个 Java 对象(包括 Class 对象)都有一个内置监视器synchronized(obj) { ... } // 使用对象监视器 synchronized(MyClass.class) { ... } // 使用类对象的监视器
-
wait/notify
必须持有锁
未在synchronized
块中调用会抛出IllegalMonitorStateException
-
虚假唤醒(Spurious Wakeup)
等待线程可能被随机唤醒(即使未调用notify
),必须用循环检查条件:while (!condition) { obj.wait(); }
💡 监视器 vs 显式锁(ReentrantLock)
特性 | 内置监视器 | ReentrantLock |
---|---|---|
实现方式 | JVM 内置 | JDK 代码实现 (java.util.concurrent) |
锁获取 | 隐式获取/释放 | 显式调用 lock() /unlock() |
等待条件 | 单一等待队列 | 支持多个 Condition 队列 |
公平性 | 非公平锁 | 可选公平/非公平模式 |
中断响应 | 不支持 | 支持 lockInterruptibly() |
📌 总结
- 监视器 = 锁 + 等待队列 + 线程调度机制
synchronized
实现锁的互斥访问wait()/notify()
实现线程间协作- 经典应用场景:生产者-消费者模型、线程间条件等待
⚠️ 注意:优先使用
java.util.concurrent
包中的高级工具(如ReentrantLock
、Condition
),它们提供更灵活的同步控制,能避免许多内置监视器的限制。
2.锁升级
在 Java 并发编程中,锁升级(Lock Escalation) 是 JVM 对 synchronized
锁的优化机制,它会根据锁竞争情况动态调整锁状态,从低开销状态逐步升级到高开销状态。这是 Java 性能优化的关键特性(自 JDK 1.6 引入)。
🔄 锁升级的四个阶段
1. 无锁状态(No Lock)
- 特征:对象刚创建时,没有任何线程访问
- 开销:无任何锁操作成本
- 对象头标志:
001
2. 偏向锁(Biased Lock)
- 适用场景:单线程重复访问同步块
- 优化原理:
- 在对象头记录首个获得锁的线程ID
- 同一线程后续进入同步块时无需 CAS 操作
- 对象头标志:
101
- 升级触发:当其他线程尝试获取锁时
3. 轻量级锁(Lightweight Lock)
- 适用场景:多线程交替执行(无实际竞争)
- 实现机制:
- 在栈帧创建锁记录(Lock Record)
- 通过 CAS 将对象头替换为指向锁记录的指针
- 成功:获得锁;失败:自旋尝试
- 对象头标志:
00
- 升级触发:自旋超过阈值(默认10次)或自旋时出现第三个线程竞争
4. 重量级锁(Heavyweight Lock)
- 适用场景:高并发竞争
- 实现机制:
- 通过操作系统 mutex 互斥量实现
- 未获锁线程进入阻塞队列(涉及内核态切换)
- 对象头标志:
10
- 特点:开销最大,但保证公平性
🧪 锁升级过程示例
public class LockEscalationDemo {private static final Object lock = new Object();private static int counter = 0;public static void main(String[] args) {// 阶段1: 偏向锁 (单线程)synchronized (lock) {counter++;}// 阶段2: 轻量级锁 (多线程交替)new Thread(() -> {for (int i = 0; i < 5; i++) {synchronized (lock) { counter++; }}}).start();// 阶段3: 重量级锁 (高并发竞争)for (int i = 0; i < 10; i++) {new Thread(() -> {synchronized (lock) { counter++; }}).start();}}
}
📊 锁状态对比表
特性 | 偏向锁 | 轻量级锁 | 重量级锁 |
---|---|---|---|
适用场景 | 单线程访问 | 多线程交替执行 | 高并发竞争 |
实现方式 | 记录线程ID | CAS自旋 | 操作系统mutex |
开销 | 极低 | 中等 | 高 |
竞争处理 | 升级为轻量级锁 | 自旋失败则升级 | 线程阻塞 |
对象头 | 存储线程ID+epoch | 指向栈中锁记录指针 | 指向监视器对象指针 |
是否阻塞 | 否 | 自旋(非阻塞) | 是(内核阻塞) |
公平性 | 无 | 无 | 可配置 |
⚙️ 锁升级关键技术细节
-
Mark Word 结构变化:
// 32位JVM对象头示例 | 锁状态 | 25bit | 4bit | 1bit(偏向) | 2bit(锁标志) | |----------|----------------|----------|------------|--------------| | 无锁 | 哈希码 | 分代年龄 | 0 | 01 | | 偏向锁 | 线程ID+epoch | 分代年龄 | 1 | 01 | | 轻量级锁 | 指向锁记录指针 | | | 00 | | 重量级锁 | 指向监视器指针 | | | 10 |
-
批量重偏向(Bulk Rebias):
- 当一类对象的偏向锁被撤销超过阈值(默认20次),JVM 会认为该类不适合偏向锁
- 后续该类的对象会直接进入轻量级锁状态
-
锁消除(Lock Elision):
-
JIT 编译器对不可能存在共享竞争的锁进行消除
// 示例:局部StringBuffer的同步会被消除 public String localMethod() {StringBuffer sb = new StringBuffer(); // 局部变量sb.append("Hello");return sb.toString(); }
-
⚠️ 重要注意事项
-
锁降级不存在:
- 锁升级是单向过程(偏向→轻量→重量)
- 一旦升级为重量级锁,不会降级(即使竞争消失)
-
偏向锁延迟启动:
-
JVM 启动后前 4 秒默认禁用偏向锁(避免初始化时的无效偏向)
# 关闭偏向锁(JDK 15+默认) -XX:-UseBiasedLocking
-
-
自旋优化:
- 轻量级锁的自旋次数由 JVM 自适应调整(Adaptive Spinning)
- 基于前一次锁获取的成功率动态变化
💡 最佳实践建议
-
低竞争场景:
- 保持默认设置(允许锁升级)
- 避免不必要的同步块
-
高竞争场景:
- 考虑使用
ReentrantLock
替代synchronized
- 利用
java.util.concurrent
高级并发工具
- 考虑使用
-
性能调优:
# 查看锁竞争情况 -XX:+PrintSynchronizationStatistics# 禁用偏向锁(若确认高竞争) -XX:-UseBiasedLocking
锁升级的本质:JVM 在线程安全和执行效率之间寻找最佳平衡点,开发者应理解其原理但避免过度干预自动优化。
3.ABA问题
ABA 问题详解
在并发编程中,ABA 问题是使用 CAS(Compare-And-Swap)操作时可能遇到的一种经典问题。它发生在共享变量的值经历了 A→B→A 的变化序列后,CAS 操作无法检测到中间状态变化的情况。
🔍 ABA 问题发生机制
问题本质
- CAS 只检查值是否匹配,不关心值是否被修改过
- 虽然最终值回到了 A,但中间状态变化被忽略
- 可能导致数据一致性问题
⚠️ 经典案例:无锁栈实现中的 ABA
public class Stack {private AtomicReference<Node> top = new AtomicReference<>();public void push(Node node) {Node oldTop;do {oldTop = top.get();node.next = oldTop;} while (!top.compareAndSet(oldTop, node));}public Node pop() {Node oldTop;Node newTop;do {oldTop = top.get();if (oldTop == null) return null;newTop = oldTop.next;} while (!top.compareAndSet(oldTop, newTop));return oldTop;}
}
ABA 问题发生场景:
- 线程1读取栈顶节点 A
- 线程1被挂起
- 线程2弹出 A,栈顶变为 B
- 线程2弹出 B
- 线程2压入 A(新节点,地址相同)
- 线程1恢复执行,CAS 成功将 A 替换为 C
- 结果:C.next 指向 B,但 B 已被弹出,造成内存错误
🛡️ ABA 问题解决方案
1. 版本号机制(推荐)
为每个状态变化添加版本号戳记:
// Java 内置解决方案
AtomicStampedReference<V> // 带整数戳记的引用
AtomicMarkableReference<V> // 带布尔标记的引用
实现原理
使用示例
public class ABASolution {private AtomicStampedReference<Integer> value = new AtomicStampedReference<>(0, 0); // 初始值=0, 版本=0public void update(int expectedValue, int newValue) {int[] stampHolder = new int[1];int oldStamp;int newStamp;do {// 读取当前值和版本int currentValue = value.get(stampHolder);oldStamp = stampHolder[0];// 验证值是否被修改过if (currentValue != expectedValue) {break; // 值已被其他线程修改}newStamp = oldStamp + 1; // 更新版本号} while (!value.compareAndSet(expectedValue, newValue, oldStamp, newStamp));}
}
2. 不重复使用内存地址
- 确保被替换的对象不会被重用
- 适用于对象池或资源管理场景
- 实现复杂,不推荐作为通用方案
3. 延迟回收(GC 语言中)
- 依赖垃圾回收机制防止对象复用
- 在非 GC 环境(如 C/C++)中不可靠
📊 ABA 问题与其他并发问题对比
问题类型 | 发生场景 | 检测难度 | 典型解决方案 |
---|---|---|---|
ABA 问题 | CAS 操作 | 高 | 版本号机制 |
竞态条件 | 多线程无序访问 | 中 | 同步锁 |
死锁 | 多锁相互等待 | 低 | 锁排序、超时机制 |
活锁 | 线程持续重试失败 | 中 | 随机退避策略 |
ABA 问题本质:CAS 操作只能检查值的相等性,无法检测值的历史变化。版本号机制通过添加状态元数据,将值检查扩展为状态机检查,从而解决这一问题。