锁(补充中..)
本文介绍锁
ReentrantLock:可重入锁(JDK提供)
Java并发基石ReentrantLock:深入解读其原理与实现-腾讯云开发者社区-腾讯云
- 最常用、最典型的
Lock实现。- 支持可重入(同一个线程可多次加锁)。
- 提供公平锁和非公平锁两种模式(构造函数可选)。
- 基于 AQS(
AbstractQueuedSynchronizer) 实现。
它的“可重入”特性意味着:如果一个线程已经持有了该锁,那么它可以在不释放锁的情况下再次调用
lock()方法而不会被阻塞,也不会报错。
示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantExample {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock(); // 第一次获取锁,hold count = 1
try {
System.out.println("第一次加锁");
lock.lock(); // 再次加锁,hold count = 2
try {
System.out.println("第二次加锁");
} finally {
lock.unlock(); // hold count = 1
}
} finally {
lock.unlock(); // hold count = 0,真正释放锁
}
}
}
ReentrantReadWriteLock:读写锁(JDK提供)
注意:ReentrantReadWriteLock 本身不直接实现 Lock 接口,但它的两个内部类 ReadLock 和 WriteLock 都实现了 Lock。
ReentrantReadWriteLock 适用于读远多于写的场景,能显著提升并发性能。合理使用读写锁,可避免 synchronized 的过度串行化问题。
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock的内部类。实现了Lock接口,代表读锁。 多个线程可同时持有读锁(只要没有写锁)。 读锁是可重入的。
ReentrantReadWriteLock.WriteLock
ReentrantReadWriteLock的另一个内部类。实现了Lock接口,代表写锁。 写锁是独占的(同一时间只能一个线程持有)。 写锁也是可重入的,并且支持锁降级(先获取写锁,再获取读锁,然后释放写锁)。
示例代码:
示例代码
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class CacheExample {
private final Map<String, String> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 读操作:多个线程可同时读
public String get(String key) {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取读锁,读取 key: " + key);
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写操作:仅一个线程可写
public void put(String key, String value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取写锁,写入 key: " + key);
try {
Thread.sleep(100); // 模拟写入耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
// 演示锁降级:先写后读(在持有写锁的情况下获取读锁)
public String getOrCreate(String key, String defaultValue) {
writeLock.lock();
try {
String value = cache.get(key);
if (value == null) {
// 模拟计算或加载
value = defaultValue + "_computed";
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入新值: " + value);
}
// 锁降级:在释放写锁前先获取读锁
readLock.lock(); // 降级关键步骤
try {
writeLock.unlock(); // 释放写锁,但仍然持有读锁
System.out.println(Thread.currentThread().getName() + " 降级为读锁,返回: " + value);
return value;
} finally {
readLock.unlock(); // 最终释放读锁
}
} finally {
// 注意:写锁已在 try 块中释放,所以这里不能再次 unlock
// 因此上面的结构必须小心处理
}
}
// 简化版(无锁降级)更安全的写法:
public String getOrCreateSafe(String key, String defaultValue) {
// 先尝试读(无锁竞争时快速返回)
readLock.lock();
String value = cache.get(key);
if (value != null) {
try {
return value;
} finally {
readLock.unlock();
}
}
readLock.unlock(); // 注意:此处需确保未在 try 中,否则可能重复 unlock
// 未命中,升级为写锁
writeLock.lock();
try {
// 双重检查(防止多个线程同时写)
value = cache.get(key);
if (value == null) {
value = defaultValue + "_computed";
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入新值: " + value);
}
// 保持写锁直到结束(不降级),或直接返回
return value;
} finally {
writeLock.unlock();
}
}
// 测试主方法
public static void main(String[] args) throws InterruptedException {
CacheExample cache = new CacheExample();
// 启动多个读线程
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
String val = cache.get("name");
if (val == null) {
cache.put("name", "Alice");
}
}, "Reader-" + id).start();
}
// 启动一个写线程
new Thread(() -> {
cache.put("age", "30");
}, "Writer").start();
Thread.sleep(2000); // 等待线程执行完成
}
}
关键点说明:
- 读锁(ReadLock):
- 多个线程可同时持有读锁。
- 读锁期间不能有写锁。
- 写锁(WriteLock):
- 独占,同一时间只能一个线程持有。
- 写锁会阻塞所有读和其他写。
- 可重入性:
- 同一线程可以多次获取读锁或写锁(计数器机制)。
- 锁降级(Lock Downgrading):
- 允许从写锁降级为读锁(先获取写锁 → 再获取读锁 → 释放写锁 → 最后释放读锁)。
- 不允许升级(即持有读锁时不能直接获取写锁,会导致死锁)。
- 双重检查(Double-Check):
- 在 getOrCreateSafe 中展示了一种更常见的“先读再写”模式,避免不必要的写锁开销。
⚠️ 注意事项:
-
不要在持有读锁时尝试获取写锁(会导致死锁,因为写锁需等待所有读锁释放,而当前线程自己持有一个读锁)。
-
锁降级代码要小心 unlock() 顺序,避免 IllegalMonitorStateException。
-
推荐优先使用 “先读再写” + 双重检查 的模式,比锁降级更简单安全。
概念
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁:
在 Java 的并发机制中,synchronized 关键字的底层实现经历了从 JDK 1.6 开始的重大优化,引入了 锁升级(Lock Elimination & Lock Coarsening) 和 四种锁状态:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
这些锁状态是 JVM 为了在不同竞争程度下平衡性能与开销而设计的。下面逐一详解:
1. 无锁(No Lock / Unlocked)
- 含义:对象未被任何线程锁定。
- Mark Word 状态:存储对象的哈希码、分代年龄等信息。
- 特点:
- 没有同步操作。
- 多个线程可自由访问共享资源(但可能产生竞态条件,需靠 volatile、CAS 等无锁编程保证安全)。
- 不是 synchronized 的状态,而是“尚未加锁”的初始状态。
✅ 示例:普通对象刚创建时就是无锁状态。
2. 偏向锁(Biased Locking)
🎯 目标
优化只有一个线程反复进入同步块的场景(比如单线程应用或低竞争环境)。
✅ 工作原理
- 当第一个线程访问同步块时,JVM 会将对象头的 Mark Word 偏向该线程 ID。
- 后续该线程再次进入同步块时,无需 CAS 操作,直接判断是否是自己,如果是就继续执行。
- 零成本获取锁(除了第一次设置偏向)。
⚠️ 注意
- 偏向锁默认在 JDK 15 被废弃,JDK 17+ 默认禁用(因为现代应用多线程竞争更常见,偏向锁反而增加复杂度)。
- 可通过
-XX:+UseBiasedLocking启用(JDK 8~14 默认开启)。
🔄 撤销偏向锁
- 当第二个线程尝试竞争锁时,JVM 会撤销偏向锁(需要 STW 安全点),升级为轻量级锁。
3. 轻量级锁(Lightweight Locking)
🎯 目标
处理短时间、低竞争的同步场景,避免直接进入 OS 互斥量(重量级锁)。
✅ 工作原理
- 线程在栈帧中创建一个 Lock Record。
- 使用 CAS 尝试将对象头的 Mark Word 替换为指向 Lock Record 的指针。
- 成功 → 获取锁(轻量级)。
- 失败 → 说明有竞争,升级为重量级锁。
- 解锁时再用 CAS 恢复原 Mark Word。
⚠️ 特点
- 自旋等待:在升级前可能短暂自旋(JDK 1.6+ 引入自旋优化)。
- 适合:同步块执行快、线程交替执行(非同时竞争)。
- 不适合:高并发、长时间持有锁(会导致大量 CAS 失败,浪费 CPU)。
4. 重量级锁(Heavyweight Locking)
🎯 目标 处理高竞争、长时间持有锁的场景。
✅ 工作原理
- 依赖操作系统的 互斥量(Mutex) 实现。
- 线程阻塞时会被挂起(进入内核态),由 OS 调度器管理。
- 阻塞/唤醒开销大(上下文切换、系统调用)。
⚠️ 特点
- 性能最差,但能保证强一致性。
- 一旦升级为重量级锁,不会降级(JDK 中锁只能升级,不能降级)。
- 对象头 Mark Word 指向 monitor 对象(ObjectMonitor),包含
_owner、_WaitSet、_EntryList等。
❗ 锁升级是单向的:偏向 → 轻量 → 重量,不会降级。
📊 对比总结
| 锁类型 | 适用场景 | 是否阻塞 | 底层机制 | 性能开销 |
|---|---|---|---|---|
| 无锁 | 无同步 | 否 | - | 最低 |
| 偏向锁 | 单线程反复进入同步块 | 否 | Mark Word 偏向 | 极低 |
| 轻量级锁 | 短时间、低竞争 | 否(自旋) | CAS + Lock Record | 低 |
| 重量级锁 | 高竞争、长时间持有 | 是 | OS Mutex | 高 |
💡 补充说明
-
锁消除(Lock Elimination):JIT 编译器发现同步代码块只被单线程访问(如局部变量),直接去掉锁。
-
锁粗化(Lock Coarsening):将多个相邻的同步块合并成一个,减少频繁加解锁开销。
-
对象头 Mark Word 结构(32位 JVM 示例):
| 25 bit hashcode | 4 bit age | 1 bit biased_lock | 2 bit lock |
lock 字段值:
01 → 无锁 / 偏向锁
00 → 轻量级锁
10 → 重量级锁
11 → GC 标记
✅ 最佳实践建议:
- 尽量减小
synchronized块范围(提高并发度)。 - 避免在同步块中做 I/O 或耗时操作(防止升级为重量级锁)。
- 在高并发场景,可考虑
ReentrantLock(支持公平锁、超时、中断等)。