目录
一、悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
核心思想:假设并发冲突必然发生,操作数据前先加锁,确保独占访问。
实现机制:
synchronized
关键字
-
JVM内置锁,自动加锁/释放锁
-
修饰方法或代码块
public synchronized void update() {
// 操作共享资源
}
ReentrantLock
显式锁
-
需手动加锁/解锁
-
提供更灵活的锁控制(可中断、公平锁等)
private final ReentrantLock lock = new ReentrantLock();
public void update() {
lock.lock();
try {
// 操作共享资源
} finally {
lock.unlock();
}
}
特点:
-
阻塞式:未获锁线程进入阻塞状态(BLOCKED)
-
保证强一致性
-
适用场景:
-
写操作频繁(冲突概率高)
-
临界区代码执行时间长
-
需要严格数据一致性(如银行转账)
-
工作流程:
二、乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
核心思想:假设并发冲突较少,操作数据时不加锁,提交更新时检测冲突。
实现机制:
CAS(Compare and Swap)
-
原子指令:
Unsafe
类提供底层支持 -
Java封装类:
AtomicInteger
、AtomicReference
等
AtomicInteger atomicInt = new AtomicInteger(0);
public void update() {
int oldValue, newValue;
do {
oldValue = atomicInt.get();
newValue = oldValue + 1;
} while (!atomicInt.compareAndSet(oldValue, newValue)); // 自旋重试
}
版本号机制
-
数据库常用(如MySQL MVCC)
-
Java实现:
AtomicStampedReference
(解决ABA问题)
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(0, 0);
public void update() {
int oldStamp, newStamp;
Integer oldValue, newValue;
do {
oldValue = ref.getReference();
oldStamp = ref.getStamp();
newValue = oldValue + 1;
newStamp = oldStamp + 1;
} while (!ref.compareAndSet(oldValue, newValue, oldStamp, newStamp));
}
特点:
-
非阻塞:线程通过自旋重试(不进入阻塞状态)
-
最终一致性
-
适用场景:
-
读多写少(冲突概率低)
-
临界区代码执行时间短
-
高并发读场景(如计数器、状态标志)
-
工作流程:
三、对比
特性 | 悲观锁 | 乐观锁 |
---|---|---|
加锁时机 | 操作数据前加锁 | 提交更新时检测冲突 |
线程状态 | 阻塞(BLOCKED) | 自旋(RUNNABLE) |
数据一致性 | 强一致性 | 最终一致性 |
实现复杂度 | 简单 | 需处理重试/ABA问题 |
性能开销 | 上下文切换成本高 | CPU自旋消耗(冲突少时高效) |
适用场景 | 写多读少、长事务 | 读多写少、短操作 |
典型实现 | synchronized 、ReentrantLock | AtomicXXX 、StampedLock |
四、选择场景
-
选择悲观锁当:
-
临界区代码复杂或执行时间长
-
写操作频率 > 读操作频率
-
需要严格保证数据实时一致性(如支付系统)
-
-
选择乐观锁当:
-
读操作频率远高于写操作(≥10:1)
-
冲突概率低于20%
-
追求高吞吐量(如电商库存计数)
-
无法承受线程阻塞开销(实时系统)
-