
在Java并发编程中,锁是解决线程安全问题的核心工具。不同场景下选择合适的锁,直接影响程序的性能与稳定性。
本文将系统梳理Java主流锁分类(包括悲观锁、乐观锁、公平锁、非公平锁、共享锁、排它锁、偏向锁、轻量级锁、自旋锁、自适应自旋锁、重量级锁、无锁),并结合代码示例讲解实现逻辑,最后给出锁选择指南与最佳实践,帮助你在实际开发中精准选型。
一、锁的分类逻辑:按维度梳理更清晰
Java锁的分类并非零散孤立,而是可按「设计思想」「核心特性」「线程行为」「JVM优化」四个维度归类。这种分类方式能帮我们更深刻理解每种锁的设计初衷,先看整体全景图: 
下面逐维度解析每种锁的定义、实现与代码示例。
二、思想维度:悲观锁、乐观锁、无锁
思想维度的锁,核心差异是「是否认为线程并发会产生冲突」,决定了锁的底层实现逻辑。
1. 悲观锁:假设冲突必然发生
定义:认为多线程并发时,数据修改冲突是必然的,因此在操作前必须先获取锁,禁止其他线程干扰。是最经典的「先锁后操作」思路。
核心实现:
synchronized:Java原生关键字,JVM层面实现,自动加锁/释放锁,无需手动管理。ReentrantLock:JUC包下的可重入锁,支持手动加锁/释放(必须在finally中释放),提供中断、超时等高级功能。ReentrantReadWriteLock.WriteLock:读写锁中的写锁,属于悲观排它锁。
代码示例(synchronized与ReentrantLock):
java
// 1. synchronized实现:方法级 + 代码块级
public class SynchronizedDemo {
private int count = 0;
// 方法级锁:锁对象为当前实例(this)
public synchronized void increment() {
count++;
System.out.println("synchronized方法:count = " + count);
}
// 代码块级锁:显式指定锁对象(可自定义,如this、Class对象)
public void decrement() {
synchronized (this) { // 锁粒度更细,只锁定核心逻辑
count--;
System.out.println("synchronized代码块:count = " + count);
}
}
}
// 2. ReentrantLock实现:手动加锁/释放,支持公平性配置
public class ReentrantLockDemo {
// 非公平锁(默认):new ReentrantLock(true) 为公平锁
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 手动加锁(若锁被占用,线程阻塞)
try {
// 核心业务逻辑:只有持有锁的线程能执行
count++;
System.out.println("ReentrantLock:count = " + count);
} finally {
// 必须在finally中释放锁,避免异常导致锁泄漏
lock.unlock();
}
}
}
2. 乐观锁:假设冲突概率极低
定义:认为多线程并发时,数据修改冲突概率很低,因此操作前不获取锁,而是在「提交修改」时检查数据是否被其他线程篡改,若未篡改则成功,否则重试/失败。核心是「先操作后校验」。
核心实现:
- CAS操作:Compare And Swap(比较并交换),CPU原子指令,是乐观锁的底层核心。
- 原子类 :
AtomicInteger、AtomicLong等,基于CAS实现无锁并发安全。 - 版本号机制 :如数据库乐观锁(通过
version字段校验)、JPA的@Version注解。 - StampedLock乐观读:JUC包下的高级锁,支持乐观读模式,性能优于传统读写锁。
代码示例(原子类与StampedLock乐观读):
java
// 1. AtomicInteger原子类:基于CAS实现无锁累加
public class AtomicDemo {
// AtomicInteger的incrementAndGet()是CAS原子操作
private final AtomicInteger count = new AtomicInteger(0);
public void safeIncrement() {
// 无需加锁,底层通过CAS保证并发安全
int newCount = count.incrementAndGet();
System.out.println("AtomicInteger:count = " + newCount);
}
// 手动CAS校验:compareAndSet(预期值, 新值)
public boolean updateIfMatch(int expect, int update) {
return count.compareAndSet(expect, update);
}
}
// 2. StampedLock乐观读:读多写少场景性能最优
public class StampedLockOptimisticDemo {
private final StampedLock lock = new StampedLock();
private double x = 0.0, y = 0.0; // 模拟共享数据
// 乐观读:无锁读取,提交前校验
public double distanceFromOrigin() {
// 1. 尝试乐观读,返回一个「邮戳」(stamp)
long stamp = lock.tryOptimisticRead();
// 2. 无锁读取数据(此时可能被其他线程修改)
double currentX = x;
double currentY = y;
// 3. 校验邮戳:若stamp有效,说明数据未被修改;否则升级为悲观读锁
if (!lock.validate(stamp)) {
// 乐观读失败,升级为悲观读锁(避免数据不一致)
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}
// 4. 计算结果(无锁/已升级为读锁,数据安全)
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 写操作:悲观排它锁
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
}
3. 无锁:完全不依赖锁的并发控制
定义:比乐观锁更极致的并发策略------完全不使用锁机制,直接通过CPU原子指令(如CAS)或线程本地存储(ThreadLocal)保证线程安全,不存在「锁竞争」问题,性能最优。
核心实现:
Atomic系列原子类(如AtomicInteger、AtomicReference)。Unsafe.CAS:sun.misc.Unsafe类的CAS方法,是原子类的底层依赖(不建议直接使用,需通过反射获取)。ThreadLocal:通过线程隔离实现无锁(每个线程持有独立副本,无共享则无竞争)。
注意:无锁并非「不需要保证安全」,而是通过非锁机制实现安全,适用于「线程间无共享数据修改」或「修改冲突极少」的场景。
三、特性维度:公平锁、非公平锁、共享锁、排它锁
特性维度的锁,核心差异是「锁的分配规则」与「锁的持有方式」,决定了锁在多线程竞争下的行为表现。
1. 公平锁 vs 非公平锁:锁的分配规则
- 公平锁:严格按照线程「申请锁的顺序」分配锁,先申请的线程先获得锁,避免线程饥饿(长期得不到锁)。
- 非公平锁:不保证申请顺序,允许「插队」------刚释放锁的线程可能直接重新获取锁(无需排队),减少线程切换开销,性能更高(默认选择)。
核心实现:
| 锁类型 | 公平锁实现 | 非公平锁实现(默认) |
|---|---|---|
| ReentrantLock | new ReentrantLock(true) | new ReentrantLock() |
| 读写锁 | new ReentrantReadWriteLock(true) | new ReentrantReadWriteLock() |
| synchronized | 不支持(始终是非公平锁) | 支持(默认) |
代码示例(公平锁与非公平锁对比):
java
public class FairVsUnfairDemo {
// 公平锁:按申请顺序分配
private final ReentrantLock fairLock = new ReentrantLock(true);
// 非公平锁:允许插队(默认)
private final ReentrantLock unfairLock = new ReentrantLock();
// 公平锁测试:先启动的线程先获得锁
public void testFairLock() {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
fairLock.lock();
try {
System.out.println("公平锁 - 获得锁的线程:" + Thread.currentThread().getName());
Thread.sleep(100); // 模拟业务耗时
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}, "Fair-Thread-" + i).start();
}
// 输出结果:Fair-Thread-0 → Fair-Thread-1 → Fair-Thread-2(严格顺序)
}
// 非公平锁测试:可能插队
public void testUnfairLock() {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
unfairLock.lock();
try {
System.out.println("非公平锁 - 获得锁的线程:" + Thread.currentThread().getName());
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unfairLock.unlock();
}
}, "Unfair-Thread-" + i).start();
}
// 输出结果:可能出现 Unfair-Thread-0 → Unfair-Thread-0(释放后直接重获)
}
}
2. 共享锁 vs 排它锁:锁的持有方式
- 共享锁(读锁):允许多个线程同时持有锁,仅限制「写操作」,不限制「读操作」。适用于「读多写少」场景(如查询数据)。
- 排它锁(写锁):仅允许一个线程持有锁,完全禁止其他线程(读/写)获取锁。适用于「写操作」(如修改数据)。
核心实现:
ReentrantReadWriteLock:JUC包下的读写锁,包含ReadLock(共享锁)和WriteLock(排它锁)。StampedLock:支持共享读锁与排它写锁,且支持乐观读升级。synchronized:仅支持排它锁,不支持共享。
代码示例(ReentrantReadWriteLock读写分离):
java
public class ReadWriteLockDemo {
// 读写锁:维护一对锁(读锁+写锁)
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 getFromCache(String key) {
readLock.lock();
try {
System.out.println("读锁 - 线程:" + Thread.currentThread().getName() + " 读取key:" + key);
return cache.get(key);
} finally {
readLock.unlock();
}
}
// 写操作:排它锁,仅单线程可写
public void putToCache(String key, String value) {
writeLock.lock();
try {
System.out.println("写锁 - 线程:" + Thread.currentThread().getName() + " 写入key:" + key);
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockDemo demo = new ReadWriteLockDemo();
// 3个读线程同时读:无冲突
for (int i = 0; i < 3; i++) {
new Thread(() -> demo.getFromCache("user1"), "Read-Thread-" + i).start();
}
// 1个写线程写:会阻塞读线程,直到写锁释放
new Thread(() -> demo.putToCache("user1", "Alice"), "Write-Thread").start();
}
}
四、行为维度:自旋锁、自适应自旋锁
行为维度的锁,核心差异是「线程等待锁时的策略」------是「忙等」还是「阻塞」,决定了锁的上下文切换开销。
1. 自旋锁:忙等不阻塞
定义:当线程请求锁被占用时,不立即阻塞(阻塞会导致CPU上下文切换,开销大),而是通过「循环重试」的方式等待锁释放(即「忙等」)。适用于「锁持有时间短、线程数少」的场景。
核心实现:
- AtomicBoolean自旋 :通过
AtomicBoolean的CAS操作实现简单自旋锁。 - JVM轻量级锁自旋 :JVM对
synchronized的优化,轻量级锁阶段会自旋重试。
代码示例(简单自旋锁实现):
java
// 基于AtomicBoolean实现的自旋锁
public class SpinLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
// 加锁:自旋重试,直到CAS成功(获取锁)
public void lock() {
// CAS尝试将locked从false改为true,失败则循环重试(自旋)
while (!locked.compareAndSet(false, true)) {
// 自旋过程:可加Thread.yield()减少CPU占用(可选)
// Thread.yield();
}
}
// 解锁:将locked设为false
public void unlock() {
locked.set(false);
}
// 测试:自旋锁的使用
public static void main(String[] args) {
SpinLock spinLock = new SpinLock();
int count = 0;
Runnable task = () -> {
spinLock.lock();
try {
for (int i = 0; i < 1000; i++) {
count++;
}
} finally {
spinLock.unlock();
}
};
new Thread(task).start();
new Thread(task).start();
// 最终count=2000(并发安全)
System.out.println("最终count:" + count);
}
}
2. 自适应自旋锁:动态调整忙等时长
定义:自旋锁的优化版------自旋次数不再是固定值,而是JVM根据「历史自旋结果」动态调整:
- 若某线程自旋成功获取锁,下次会增加自旋次数(认为后续仍可能成功)。
- 若某线程自旋多次失败,下次会减少自旋次数甚至直接阻塞(避免无效忙等)。
核心实现:
- JVM内置实现:
synchronized的轻量级锁阶段默认使用自适应自旋,无需手动编码。 - 优势:兼顾「短锁持有时间」的自旋收益与「长锁持有时间」的阻塞开销,性能更优。
五、JVM锁优化:偏向锁、轻量级锁、重量级锁
这三类锁并非独立锁类型,而是 synchronized的「锁升级路径」------JVM根据「锁竞争激烈程度」自动切换锁状态,实现性能最优。
1. 锁升级逻辑
synchronized的锁状态存储在对象头(Mark Word)中,升级路径为: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁(只能升级,不能降级)。
| 锁状态 | 适用场景 | 实现逻辑 |
|---|---|---|
| 偏向锁 | 无竞争(单线程重复加锁) | 给对象头标记「线程ID」,后续该线程加锁无需CAS,直接复用锁。 |
| 轻量级锁 | 轻微竞争(多线程交替加锁) | 线程将对象头Mark Word复制到栈帧,通过CAS竞争锁,竞争失败则自旋重试。 |
| 重量级锁 | 激烈竞争(多线程同时抢锁) | 依赖OS互斥量(Mutex)实现,竞争失败的线程直接阻塞,避免无效自旋。 |
2. 代码示例(synchronized锁升级演示)
java
public class LockUpgradeDemo {
private final Object lock = new Object(); // 锁对象
private int count = 0;
public void demonstrateUpgrade() {
// 1. 无竞争 → 偏向锁:单线程重复加锁
synchronized (lock) {
count++;
System.out.println("阶段1:偏向锁(单线程加锁),count = " + count);
}
// 2. 轻微竞争 → 轻量级锁:多线程交替加锁(无同时抢锁)
Thread t1 = new Thread(() -> {
synchronized (lock) {
count++;
System.out.println("阶段2:轻量级锁(t1加锁),count = " + count);
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
count--;
System.out.println("阶段2:轻量级锁(t2加锁),count = " + count);
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3. 激烈竞争 → 重量级锁:多线程同时抢锁
System.out.println("阶段3:开始激烈竞争,升级为重量级锁");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
count++;
System.out.println("重量级锁 - 线程:" + Thread.currentThread().getName() + ",count = " + count);
}
}, "Heavy-Thread-" + i).start();
}
}
public static void main(String[] args) {
new LockUpgradeDemo().demonstrateUpgrade();
}
}
六、锁选择指南:场景化选型建议
掌握锁分类后,核心是「根据场景选对锁」。以下是常见场景的推荐方案:
| 业务场景 | 推荐锁类型 | 选型理由 |
|---|---|---|
| 简单同步逻辑(如单例、计数器) | synchronized |
JVM自动优化(偏向/轻量/重量级),代码简洁,无锁泄漏风险。 |
| 需要中断/超时/公平锁 | ReentrantLock |
支持lockInterruptibly()(可中断)、tryLock(long)(超时)、公平配置。 |
| 读多写少(如缓存、报表) | ReentrantReadWriteLock / StampedLock |
读操作并发执行,StampedLock乐观读性能优于传统读写锁。 |
| 高并发计数器(如接口调用量) | AtomicLong / LongAdder |
CAS无锁操作,性能远超synchronized,LongAdder在高并发下更优。 |
| 高并发集合(如并发缓存) | ConcurrentHashMap |
底层用「分段锁+CAS」,支持高并发读写,避免全表锁。 |
| 资源池控制(如连接池) | Semaphore |
精确控制并发线程数(如限制10个线程同时获取连接)。 |
七、最佳实践:避免锁坑,提升性能
- 优先用
synchronized:除非需要ReentrantLock的高级功能(如中断、超时),否则优先选synchronized------JVM持续优化(如JDK 1.6后引入偏向锁/轻量级锁),且无需手动释放。 - 缩小锁粒度:只锁定「核心业务逻辑」,避免锁范围过大(如不要在锁内执行IO操作)。
- 避免死锁 :多锁竞争时,按「固定顺序」获取锁(如按锁对象的hashCode排序),或使用
tryLock()超时机制。 - 读写分离优先 :读多写少场景必用读写锁(
ReentrantReadWriteLock/StampedLock),避免读操作阻塞读操作。 - 监控锁竞争 :用JMX(如
java.lang.management.LockInfo)或APM工具(如SkyWalking)监控锁等待时间,及时优化高竞争锁。 - 慎用乐观锁重试:乐观锁重试次数不宜过多(建议3-5次),失败后可降级为悲观锁或返回友好提示(避免CPU空转)。
八、总结
Java锁的分类并非孤立,而是从「思想」「特性」「行为」「JVM优化」四个维度层层递进。核心记住三点:
- 无锁/乐观锁 性能最优,但适用场景有限;悲观锁通用性强,但需注意锁竞争。
- synchronized 是基础,JVM自动优化;JUC锁 (如
ReentrantLock、StampedLock)功能更丰富,需手动管理。 - 选型核心是场景 :读多写少用读写锁,简单同步用
synchronized,高并发计数用原子类。
希望本文能帮你理清Java锁的脉络,在实际开发中避开锁坑,写出高效、安全的并发代码!如果有疑问或补充,欢迎在评论区交流~