💡 摘要:你是否知道
synchronized
锁对象时,锁的到底是什么?
是否在ReentrantLock
的tryLock()
和lockInterruptibly()
之间困惑?
Java 中的“锁”远不止synchronized
,
从 JVM 内置锁 到 JUC 显式锁,
从 悲观锁 到 乐观锁,
从 可重入、公平性 到 锁升级,
本文将带你深入剖析 Java 锁的 7 大核心机制,
揭秘synchronized
的 偏向锁 → 轻量级锁 → 重量级锁 升级过程,
解析ReentrantLock
基于 AQS (AbstractQueuedSynchronizer) 的实现原理,
对比synchronized
与ReentrantLock
的性能与适用场景,
并介绍StampedLock
、ReadWriteLock
等高级锁。
文末附锁选型决策树与面试高频问题,
助你彻底掌握 Java 并发的“定海神针”。
一、锁的本质:保护共享资源
在多线程环境下,多个线程并发访问共享的可变资源时,必须通过锁机制来保证原子性、可见性、有序性,防止数据不一致。
public class Counter {
private int count = 0;
// ❌ 线程不安全!
public void unsafeIncrement() {
count++; // 读 → 加 → 写,非原子
}
// ✅ 线程安全
public synchronized void safeIncrement() {
count++; // synchronized 保证了原子性
}
}
🔐 锁的作用:确保同一时刻只有一个线程能进入临界区。
二、Java 锁的分类
分类维度 | 类型 | 说明 |
---|---|---|
实现方式 | 内置锁 (Intrinsic Lock) | synchronized 关键字 |
显式锁 (Explicit Lock) | java.util.concurrent.locks.Lock 接口实现 | |
获取方式 | 悲观锁 (Pessimistic Lock) | 假设冲突,先加锁再操作 (synchronized , ReentrantLock ) |
乐观锁 (Optimistic Lock) | 假设无冲突,操作后检查 (CAS , Atomic 类) | |
可重入性 | 可重入锁 (Reentrant Lock) | 同一线程可重复获取 (synchronized , ReentrantLock ) |
不可重入锁 | 同一线程重复获取会死锁(Java 中较少见) | |
公平性 | 公平锁 (Fair Lock) | 遵循 FIFO,等待时间长的优先获取 |
非公平锁 (Non-fair Lock) | 不保证顺序,可能“插队” | |
共享性 | 独占锁 (Exclusive Lock) | 写锁,一次只允许一个线程持有 |
共享锁 (Shared Lock) | 读锁,允许多个线程同时持有 |
1. synchronized
:JVM 内置锁
✅ 语法与锁对象
public class SynchronizedDemo {
private final Object lock = new Object();
// 1. 锁实例对象 (this)
public synchronized void method1() {
// ...
}
// 2. 锁类对象 (SynchronizedDemo.class)
public static synchronized void method2() {
// ...
}
// 3. 锁指定对象
public void method3() {
synchronized (lock) {
// ...
}
}
// 4. 锁代码块 (this)
public void method4() {
synchronized (this) {
// ...
}
}
}
🔑 关键:
synchronized
锁的是对象,而不是代码块。
✅ 特性
- 可重入:同一线程可多次进入同一锁。
- 自动释放:异常或方法结束时自动释放。
- 非公平锁:不保证等待线程的顺序。
🔬 synchronized
的锁升级(JDK 1.6+)
为了优化性能,JVM 对 synchronized
进行了深度优化,实现了锁升级:
-
偏向锁 (Biased Locking):
- 当一个线程第一次获取锁时,JVM 会将对象头的 Mark Word 设置为偏向该线程。
- 后续该线程进入同步块时,只需检查 Mark Word 是否偏向自己,无需同步操作。
- 优点:极致优化单线程场景。
-
轻量级锁 (Lightweight Locking):
- 当有第二个线程竞争锁时,偏向锁升级为轻量级锁。
- 使用 CAS 操作将对象头的 Mark Word 替换为指向线程栈中锁记录的指针。
- 竞争线程通过自旋(循环尝试获取锁)等待,避免线程阻塞。
- 优点:适用于锁竞争不激烈、持有时间短的场景。
-
重量级锁 (Heavyweight Locking):
- 当自旋一定次数仍未获取锁(或竞争激烈),轻量级锁升级为重量级锁。
- 依赖操作系统互斥量(Mutex)实现,线程无法获取锁时进入阻塞状态,由操作系统调度。
- 缺点:线程阻塞/唤醒开销大。
✅ 锁升级是单向的:一旦升级为重量级锁,不会降级。
2. ReentrantLock
:可重入显式锁
✅ 基本用法
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在 finally 中释放!
}
}
⚠️ 重要:
unlock()
必须在finally
块中调用,否则异常时锁无法释放,导致死锁。
✅ 核心优势(相比 synchronized
)
特性 | synchronized | ReentrantLock |
---|---|---|
可中断 | ❌ | ✅ lockInterruptibly() |
超时获取 | ❌ | ✅ tryLock(timeout) |
公平锁 | ❌ (非公平) | ✅ 构造函数可指定 |
条件变量 | 1个 (wait/notify ) | ✅ 多个 (Condition ) |
锁状态检查 | ❌ | ✅ isLocked() , getHoldCount() |
✅ 示例:可中断与超时
// 1. 可中断获取锁
public void interruptibleMethod() throws InterruptedException {
lock.lockInterruptibly(); // 可响应中断
try {
// ...
} finally {
lock.unlock();
}
}
// 2. 超时获取锁
public boolean timedMethod() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// ...
} finally {
lock.unlock();
}
return true;
} else {
System.out.println("获取锁超时");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
🔬 ReentrantLock
的核心:AQS (AbstractQueuedSynchronizer)
ReentrantLock
的核心是 AQS 框架。- AQS 使用一个
volatile int state
表示同步状态(0=无锁,1=已锁,>1=可重入计数)。 - 使用 CLH 队列(FIFO 队列)管理等待线程。
- 获取锁失败的线程会被包装成
Node
加入队列,并通过LockSupport.park()
阻塞。 - 释放锁时,唤醒队列中的下一个节点。
✅ AQS 是 JUC 并发包的基石,
Semaphore
,CountDownLatch
,ReentrantReadWriteLock
等都基于 AQS 实现。
3. ReentrantReadWriteLock
:读写锁
✅ 场景
适用于读多写少的场景,提高并发性能。
- 读锁 (Shared Lock):允许多个线程同时读。
- 写锁 (Exclusive Lock):写操作时,其他读写操作均被阻塞。
✅ 用法
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private Map<String, String> cache = new HashMap<>();
public String get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
⚠️ 注意:
- 读锁不能升级为写锁(会导致死锁)。
- 写锁可以降级为读锁。
4. StampedLock
:更高效的读写锁(JDK 8+)
✅ 优势
- 性能优于
ReentrantReadWriteLock
。 - 支持乐观读 (Optimistic Reading):读操作不加锁,最后检查是否有写操作。
✅ 用法
private final StampedLock stampedLock = new StampedLock();
private double x, y;
// 1. 写锁
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp);
}
}
// 2. 乐观读
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 尝试乐观读
double currentX = x, currentY = y;
if (!stampedLock.validate(stamp)) { // 检查期间是否有写操作
stamp = stampedLock.readLock(); // 升级为悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 3. 悲观读锁
public double readWithReadLock() {
long stamp = stampedLock.readLock();
try {
return Math.sqrt(x * x + y * y);
} finally {
stampedLock.unlockRead(stamp);
}
}
✅ 适用场景:读操作非常频繁,且写操作极少。
5. 乐观锁:CAS 与 Atomic
类
✅ 核心:CAS (Compare-And-Swap)
- CPU 指令级原子操作。
- 操作:
if (value == expected) then value = newValue else retry
- Java 通过
Unsafe
类实现。
✅ Atomic
类族
AtomicInteger
,AtomicLong
,AtomicBoolean
AtomicReference
,AtomicStampedReference
(解决 ABA 问题)AtomicLongArray
,AtomicIntegerFieldUpdater
(反射更新)
private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // 原子自增
}
public int get() {
return counter.get();
}
✅ ABA 问题
- 值从 A → B → A,CAS 认为未变,但中间状态已改变。
- 解决方案:
AtomicStampedReference
或AtomicMarkableReference
,引入版本号或标记。
三、锁的性能与选型
性能对比(简要)
锁类型 | 低竞争 | 高竞争 | 说明 |
---|---|---|---|
synchronized | ✅✅ | ✅ | JDK 1.6+ 优化后性能很好 |
ReentrantLock | ✅✅✅ | ✅✅ | 功能丰富,性能略优 |
ReentrantReadWriteLock | ✅✅✅ (读) | ✅ (写) | 读多写少场景优势明显 |
StampedLock | ✅✅✅✅ | ✅✅ | 乐观读性能极高 |
Atomic 类 | ✅✅✅✅✅ | ✅✅✅✅ | 无锁,高并发计数首选 |
🔬 JDK 1.6+ 的
synchronized
经过锁升级优化后,性能已接近ReentrantLock
。
四、最佳实践
- 优先使用
synchronized
:简单、安全、JVM 优化好。 - 高并发计数用
Atomic
类:无锁,性能高。 - 读多写少用
ReentrantReadWriteLock
或StampedLock
。 - 需要精确控制用
ReentrantLock
。 - 避免锁的过度嵌套,防止死锁。
- 缩小锁的粒度,减少临界区代码。
- 避免在锁内进行耗时操作(如 I/O、网络调用)。
五、高频问题
❓1. synchronized
锁升级的过程是怎样的?
答:
- 无锁 → 2. 偏向锁(偏向首个线程)→ 3. 轻量级锁(CAS + 自旋)→ 4. 重量级锁(Mutex 阻塞)。
升级是单向的,由 JVM 自动完成。
❓2. ReentrantLock
是如何实现可重入的?
答:
基于 AQS 的state
变量。
每次成功获取锁,state++
;每次释放锁,state--
。
同一线程可多次获取,state
记录重入次数。
❓3. ReentrantLock
和 synchronized
的区别?
答:
- 实现:
synchronized
是 JVM 内置,ReentrantLock
是 API 实现。- 功能:
ReentrantLock
支持中断、超时、公平锁、多个 Condition。- 释放:
synchronized
自动释放,ReentrantLock
需手动释放。- 性能:JDK 1.6+ 后性能接近。
❓4. 什么是 AQS?ReentrantLock
如何使用它?
答:
AQS (AbstractQueuedSynchronizer) 是构建锁和同步器的框架。
ReentrantLock
使用 AQS 的state
表示锁状态,
使用 CLH 队列管理等待线程,
通过CAS
操作修改state
,
获取失败的线程入队并阻塞,释放时唤醒后继。
❓5. StampedLock
的乐观读是如何工作的?
答:
- 调用
tryOptimisticRead()
获取一个时间戳 (stamp)。- 执行读操作。
- 调用
validate(stamp)
检查期间是否有写操作。- 如果
validate
返回true
,读成功;否则需升级为悲观读锁。
六、总结
锁类型 | 推荐度 | 适用场景 |
---|---|---|
synchronized | ✅✅ | 通用同步,简单场景 |
ReentrantLock | ✅✅✅ | 需要高级功能 |
ReentrantReadWriteLock | ✅✅✅ | 读多写少 |
StampedLock | ✅✅✅✅ | 读极多写极少 |
Atomic 类 | ✅✅✅✅✅ | 高并发计数/状态 |
✅ 终极建议:
- 简单同步首选
synchronized
。- 复杂需求用
ReentrantLock
。- 读多写少用
StampedLock
。- 计数用
Atomic
。- 理解锁的原理(AQS, CAS, 锁升级)是进阶的关键。
掌握这些锁机制,你就能在 Java 并发编程的海洋中游刃有余!