锁的定义和核心意义
在多线程并发编程中,锁是协调线程对共享资源访问的核心机制。其核心目标在于:
- 原子性:确保临界区代码的不可分割执行
- 可见性:保证共享变量的修改对其他线程立即可见
- 有序性:防止指令重排序导致的执行顺序问题
Java中的锁实现可分为两大类:
- JVM内置锁:通过
synchronized关键字实现 - 显式锁:
java.util.concurrent.locks包下的各种锁实现
锁的分类
一、按照 线程是否阻塞 分类
| 类型 | 核心思想 | 典型实现 | 适用场景 |
|---|---|---|---|
| 悲观锁 | 假定一定会发生竞争,每次访问数据前先加锁 | synchronized、ReentrantLock | 写操作多、竞争激烈场景 |
| 乐观锁 | 假定不会发生竞争,通过版本号/CAS机制保证原子性 | AtomicInteger、StampedLock | 读多写少、低竞争场景 |
二、按照 公平性 分类
| 类型 | 特点 | 实现类 | 性能对比 |
|---|---|---|---|
| 公平锁 | 严格按请求顺序分配锁,避免线程饥饿 | new ReentrantLock(true) | 吞吐量较低,响应时间稳定 |
| 非公平锁 | 允许插队获取锁,可能引发线程饥饿 | synchronized、默认ReentrantLock | 吞吐量高,响应时间波动较大 |
三、按照 可重入性 分类
| 类型 | 特点 | 实现示例 | 优势 |
|---|---|---|---|
| 可重入锁 | 同一线程可重复获取同一锁(通过计数器实现) | ReentrantLock、synchronized | 避免递归调用时的死锁 |
| 不可重入锁 | 同一线程多次获取锁会阻塞自身 | 早期ThreadLocal的部分实现 | 实现简单,但易导致死锁 |
四、按照 共享性 分类
| 类型 | 特点 | 典型实现 | 使用场景 |
|---|---|---|---|
| 独占锁 | 同一时刻只能被一个线程持有 | ReentrantLock、synchronized | 写操作等排他性场景 |
| 共享锁 | 允许多个线程同时持有锁 | ReentrantReadWriteLock.ReadLock | 读操作等共享场景 |
五、按照 实现原理 分类
| 类型 | 实现机制 | 特点 | 典型示例 |
|---|---|---|---|
| 自旋锁 | 通过循环尝试CAS操作(while(!tryLock())) | 避免线程切换,但消耗CPU | AtomicBoolean底层实现 |
| 适应性自旋锁 | 根据前次等待时间动态调整自旋次数 | 平衡CPU消耗与等待时间 | HotSpot JVM优化实现 |
| CLH锁 | 基于队列的公平锁实现(Craig, Landin, Hagersten) | 公平性好,空间效率高 | AQS底层实现 |
| MCS锁 | 基于显式链表的自旋锁(改良CLH) | 减少缓存同步开销 | 部分JVM实现 |
六、按照 锁优化技术 分类
| 类型 | 实现原理 | 优化目标 |
|---|---|---|
| 偏向锁 | 记录第一个获取锁的线程ID,后续无竞争直接访问 | 消除无竞争时的同步开销 |
| 轻量级锁 | 通过线程栈中的Lock Record进行CAS竞争 | 减少重量级锁的系统调用开销 |
| 锁消除 | JIT编译器消除不可能存在共享竞争的锁 | 减少不必要的同步操作 |
| 锁粗化 | 合并多个相邻同步块为单个大同步块 | 减少频繁加锁/解锁的开销 |
七、按照 锁粒度 分类
| 类型 | 特点 | 典型实现 |
|---|---|---|
| 粗粒度锁 | 锁住整个数据结构或大范围资源 | Vector的全表锁 |
| 细粒度锁 | 对数据结构的局部加锁 | ConcurrentHashMap的分段锁 |
| 无锁结构 | 完全不使用锁,通过CAS实现线程安全 | CopyOnWriteArrayList |
八、特殊用途锁
| 类型 | 特点 | 典型实现 | 使用场景 |
|---|---|---|---|
| 条件锁 | 通过Condition实现线程的精确唤醒 | ReentrantLock.newCondition() | 生产者-消费者模型 |
| 读写锁 | 分离读锁(共享)与写锁(独占) | ReentrantReadWriteLock | 读多写少场景 |
| 信号量 | 控制同时访问资源的线程数量 | Semaphore | 连接池、限流场景 |
| 倒计时门闩 | 等待多个线程完成指定操作 | CountDownLatch | 多线程任务协同 |
九、按 锁状态 分类
| 类型 | 特点 | 实现机制 |
|---|---|---|
| 可中断锁 | 等待锁时能响应中断请求 | lockInterruptibly() |
| 不可中断锁 | 等待锁时不响应中断 | synchronized |
十、按 锁持有时间 分类
| 类型 | 特点 | 典型场景 |
|---|---|---|
| 短期锁 | 持有时间极短(纳秒级) | 自旋锁、CAS操作 |
| 长期锁 | 持有时间较长(毫秒级以上) | 数据库事务锁 |
总结:锁的选择原则
- 竞争程度:低竞争用乐观锁,高竞争用悲观锁
- 读写比例:读多写少用读写锁,写多用独占锁
- 公平需求:严格顺序用公平锁,高吞吐用非公平锁
- 锁粒度:根据数据结构特点选择粗/细粒度锁
- 可重入性:递归调用必须用可重入锁
不同的锁类型在Java并发包中都有典型实现(如ReentrantLock、StampedLock等),理解这些分类有助于在实际开发中选择最合适的同步机制。
锁的原理
synchronized的底层实现
synchronized的底层实现经历了多次优化,核心机制为对象头Mark Word:
1. 对象内存布局
|--------------------------------------------------------------|
| Object Header (64 bits) | Instance Data |
|------------------------|---------------------|------------------|
| Mark Word (32bits) | Klass Word (32bits)| |
|---------------------------------------------------------------|
2. Mark Word结构(32位JVM)
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
3. 锁升级过程
-
偏向锁(Biased Locking)
- 适用场景:单线程访问同步块
- 原理:在Mark Word中记录线程ID
- 优势:无竞争时的零成本加锁
-
轻量级锁(Lightweight Locking)
- 适用场景:低竞争的多线程环境
- 实现:通过CAS操作将Mark Word复制到线程栈中的Lock Record
-
重量级锁(Heavyweight Locking)
- 适用场景:高竞争环境
- 实现:通过操作系统的互斥量(mutex)实现
- 特点:线程阻塞和唤醒涉及内核态切换
AQS(AbstractQueuedSynchronizer)核心原理
Java显式锁的基石,采用CLH队列变体实现同步控制。
1. 核心组件
// 同步状态
private volatile int state;
// CLH队列节点
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
2. 关键方法
tryAcquire(int):尝试获取独占锁tryRelease(int):尝试释放独占锁tryAcquireShared(int):尝试获取共享锁tryReleaseShared(int):尝试释放共享锁
3. 获取锁流程
+---------------------+
| 尝试获取锁(tryAcquire) |
+----------+----------+
|
v
+----------+----------+
| 成功? +--否--> 加入等待队列
+----------+----------+
|
v
获取锁成功
ReentrantLock实现剖析
1. 核心结构
public class ReentrantLock implements Lock {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 实现AQS的tryAcquire等方法
}
static final class NonfairSync extends Sync { /* 非公平实现 */ }
static final class FairSync extends Sync { /* 公平实现 */ }
}
2. 公平锁 vs 非公平锁
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取顺序 | 严格FIFO | 允许插队 |
| 吞吐量 | 较低 | 较高 |
| 实现差异 | hasQueuedPredecessors()检查 | 直接尝试CAS获取 |
3. 可重入实现 通过维护当前持有锁的线程和重入次数:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
读写锁(ReentrantReadWriteLock)
1. 状态分割
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 高16位:读锁计数
// 低16位:写锁计数
2. 锁降级示例
readLock.lock();
try {
if (!cacheValid) {
writeLock.lock(); // 必须先释放读锁才能获取写锁
try {
// 更新缓存
cacheValid = true;
} finally {
writeLock.unlock();
}
}
// 使用缓存数据
} finally {
readLock.unlock();
}
乐观锁与CAS
1. CAS原理
// 伪代码实现
bool compare_and_swap(int *addr, int expected, int new_value) {
if (*addr == expected) {
*addr = new_value;
return true;
}
return false;
}
2. Java中的实现
public final class Unsafe {
public final native boolean compareAndSwapInt(
Object o, long offset, int expected, int x);
}
3. ABA问题解决方案
使用AtomicStampedReference:
AtomicStampedReference<Integer> atomicRef =
new AtomicStampedReference<>(100, 0);
int[] stampHolder = new int[1];
int currentStamp = atomicRef.get(stampHolder);
atomicRef.compareAndSet(100, 101, currentStamp, currentStamp + 1);
锁性能优化策略
-
减少锁粒度
- ConcurrentHashMap的分段锁设计
-
锁消除
public String concat(String s1, String s2) { return s1 + s2; // JIT编译器可能消除StringBuffer的同步 } -
锁粗化
// 多次连续的加锁/解锁操作合并为单次操作 synchronized(lock) { // 多个操作合并 } -
线程本地化
- 使用ThreadLocal避免共享资源竞争
锁选择决策树
是否需要等待可中断? → 选择Lock
↓否
需要尝试非阻塞获取? → 选择tryLock()
↓否
需要公平性保证? → 选择公平锁
↓否
读多写少场景? → 选择读写锁
↓否
短期锁定? → 选择自旋锁
↓否
使用synchronized
死锁预防策略
- 顺序加锁法
- 超时机制
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { try { // 临界区 } finally { lock.unlock(); } } - 资源排序法
- 死锁检测(使用jstack或JMX)
总结
Java的锁机制是一个从语言层面到JVM实现的多层次体系:
- 内置锁:简单易用,自动管理
- 显式锁:灵活可控,功能丰富
- 原子变量:无锁编程的基础
- 锁优化:适应不同场景的性能需求
正确选择和使用锁需要综合考虑:
- 竞争激烈程度
- 读写比例
- 线程等待的容忍度
- 系统吞吐量要求