在并发开发和数据库事务处理中,锁机制是绕不开的难题。了解锁的种类、原理和应用场景,对于写出高效安全的并发程序非常重要。本文将系统总结公平锁、非公平锁、悲观锁、乐观锁、可重入锁、锁的升级等核心知识,并配套详细的实用代码,带你搞懂常见锁机制!
一、公平锁与非公平锁
1. 公平锁(Fair Lock)
定义:获取锁的顺序按照线程到来的顺序进行分配,先请求锁的线程先获得锁,避免线程“饥饿”。
应用场景:对执行先后顺序非常敏感的场合,如任务调度、并发队列等。
代码示例(Java):
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo {
// true 表示公平锁
private static final ReentrantLock fairLock = new ReentrantLock(true);
public void testLock() {
fairLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得了公平锁");
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}
public static void main(String[] args) {
FairLockDemo demo = new FairLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(demo::testLock, "Thread-" + i).start();
}
}
}
2. 非公平锁(Nonfair Lock)
定义:线程加锁的顺序不严格按照申请顺序,允许“插队”,提高吞吐量。
应用场景:大多数并发场景,追求性能。
代码示例(Java):
import java.util.concurrent.locks.ReentrantLock;
public class NonFairLockDemo {
// 默认 false,非公平锁
private static final ReentrantLock lock = new ReentrantLock();
public void testLock() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获得了非公平锁");
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
NonFairLockDemo demo = new NonFairLockDemo();
for (int i = 0; i < 5; i++) {
new Thread(demo::testLock, "Thread-" + i).start();
}
}
}
拓展: synchronized
在 JVM 层面也是一种非公平锁。
二、悲观锁与乐观锁
1. 悲观锁(Pessimistic Lock)
定义:假设系统“很可能”发生并发冲突,每次数据操作都先加锁,其他线程只能等待。
应用场景:并发写多于读,且数据一致性要求高,例如订单、资金等核心业务。
数据库实现:
-- 事务中加锁(MySQL InnoDB)
START TRANSACTION;
SELECT * FROM account WHERE id=1 FOR UPDATE;
-- do something...
UPDATE account SET balance=balance-100 WHERE id=1;
COMMIT;
Java实现(synchronized / Lock):
public class PessimisticLockDemo {
public synchronized void doSomething() {
System.out.println(Thread.currentThread().getName() + " 获得了悲观锁");
// 执行“临界区”操作
}
}
2. 乐观锁(Optimistic Lock)
定义:假定冲突较少,不上锁,通过“版本号”或CAS检查数据是否被别人修改。若被修改则重试。
应用场景:用户数大、并发冲突不多的场景,如商品秒杀、用户信息查询等。
数据库实现(版本号字段):
-- 查询时一并获取 version,例如5
UPDATE account SET balance=balance-100, version=version+1 WHERE id=1 AND version=5;
-- 如果该SQL更新数为0,说明数据已被别的线程改过,需要重试
Java实现(CAS 原子类):
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockDemo {
private AtomicInteger value = new AtomicInteger(0);
public void tryUpdate() {
int oldVal, newVal;
do {
oldVal = value.get();
newVal = oldVal + 1;
} while (!value.compareAndSet(oldVal, newVal));
System.out.println("成功更新为:" + newVal);
}
}
三、可重入锁(Reentrant Lock)
定义:同一线程多次申请同一把锁不会被阻塞,内部会维护锁“重入次数”。
作用:方便递归、父子方法重复加锁的场景,避免死锁。
1. synchronized的可重入性:
public class ReentrantSyncDemo {
public synchronized void outer() {
System.out.println("outer....");
inner();
}
public synchronized void inner() {
System.out.println("inner....");
}
public static void main(String[] args) {
new ReentrantSyncDemo().outer();
}
}
2. ReentrantLock的可重入性:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("进入methodA");
methodB();
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
System.out.println("进入methodB");
} finally {
lock.unlock();
}
}
}
四、锁的升级(锁的优化机制)
定义:JVM对synchronized
的性能优化,锁的状态会随竞争程度自动“升级”,包括偏向锁、轻量级锁和重量级锁。
锁的状态与升级流程:
- 无锁:对象未被线程持有。
- 偏向锁:假设只有一条线程反复获得锁,极高性能(无任何同步原语)。
- 轻量级锁:短期竞争时的自旋锁(无需阻塞,性能高)。
- 重量级锁:竞争激烈时的传统监视器锁(阻塞/唤醒)。
状态升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
(注意:不会降级)
代码观察(JDK源码、JOL工具辅助理解):
public class LockUpgradeDemo {
public static void main(String[] args) throws Exception {
Object obj = new Object();
synchronized (obj) {
// 查看对象头信息
System.out.println("进入同步块");
}
}
}
使用JOL(Java Object Layout)库可以详细观察对象头信息及锁状态变化。
五、总结对比表
类型 | 定义 | 场景 | 代码说明 | 优缺点 |
---|---|---|---|---|
公平锁 | 先到先得,排队获取锁 | 银行排队 | new ReentrantLock(true) | 避免饿死但慢 |
非公平锁 | 不按顺序,允许插队 | 绝大多数并发场景 | new ReentrantLock(false) 或synchronized | 性能高风险饿死 |
悲观锁 | 先锁后用,强制同步 | 核心交易、资金 | synchronized /数据库for update | 安全性能一般 |
乐观锁 | 无锁冲突检测后重试 | 高频读、低冲突 | Java原子类、版本号字段 | 快,不适用多冲突 |
可重入锁 | 自身可多次获得同一锁 | 递归与多调用场景 | synchronized 、ReentrantLock | 灵活无死锁 |
锁的升级 | JVM智能优化锁粒度 | JVM自动控制 | synchronized (自动实现) | 性能最优动态选择 |
六、开发实践
- 公平锁用于严格保证顺序,通常业务场景非必须;
- 乐观锁适用于冲突概率小、性能要求高的场合;
- 悲观锁安全但性能低,不适于高并发的热点资源;
- 可重入锁极大提高编码灵活性,避免递归死锁;
- JVM锁优化机制让大部分场合无需人工干涉锁粒度;
- 项目开发建议优先选择简单的
synchronized
,高并发场景评估使用ReentrantLock
及乐观锁。