Java 并发编程进阶:ReentrantLock 实现原理详解
目录
七、ReentrantLock 与 synchronized 对比
一、与synchronized区别
在 Java 并发编程中,最常见的加锁方式莫过于 synchronized
public synchronized void add() {
count++;
}
synchronized 使用简单,并且由 JVM 保证锁的获取与释放,是 Java 中最基础的线程同步机制
不过随着并发场景越来越复杂,synchronized 的局限性也逐渐显现出来:
-
无法尝试获取锁
-
无法设置获取锁超时时间
-
无法中断等待锁的线程
-
不支持公平锁
-
只能使用单一条件队列
为了解决这些问题,JDK 在 java.util.concurrent.locks 包中提供了 Lock 接口,并实现了最常用的显式锁 —— ReentrantLock
Lock lock = new ReentrantLock();
从功能上来说,ReentrantLock 与 synchronized 都属于独占锁,但它提供了更加灵活的锁控制能力
二、ReentrantLock 的核心特性
1、可重入
同一个线程获取锁后,可以再次获取同一把锁而不会发生死锁
例如:
public void methodA() {
lock.lock();
try {
methodB();
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
} finally {
lock.unlock();
}
}
methodA 获取锁后调用 methodB
如果锁不支持重入,那么 methodB 会再次申请同一把锁,从而陷入永久等待
而 ReentrantLock 允许当前持有锁的线程重复获取锁,因此不会发生死锁
2、公平锁与非公平锁
ReentrantLock 提供两种模式:
默认使用非公平锁
new ReentrantLock();
使用公平锁
new ReentrantLock(true);
公平锁要求线程按照等待顺序获取锁:
Thread-A
↓
Thread-B
↓
Thread-C
A 释放锁后,必须由 B 获取
而非公平锁允许线程插队:
A释放锁
↓
新线程D直接获得锁
↓
B继续等待
虽然非公平锁可能导致部分线程等待时间较长,但由于减少了线程切换,因此具有更高的吞吐量
这也是 ReentrantLock 默认采用非公平策略的原因
3、尝试获取锁
synchronized 获取不到锁时只能阻塞等待
而 ReentrantLock 提供了 tryLock:
if (lock.tryLock()) {
try {
} finally {
lock.unlock();
}
}
获取成功返回 true,获取失败立即返回 false,不会进入阻塞状态
4、可中断获取锁
普通 lock() 在等待期间无法响应中断
lock.lock();
而 lockInterruptibly() 可以:
lock.lockInterruptibly();
如果线程在等待锁期间收到中断信号:
thread.interrupt();
会立即抛出 InterruptedException,这在死锁检测、超时控制等场景中非常有用
5、Condition 条件队列
synchronized 配套的是:
wait()
notify()
notifyAll()
而 ReentrantLock 提供:
Condition condition = lock.newCondition();
配合:
condition.await();
condition.signal();
condition.signalAll();
实现线程通信
并且一个 ReentrantLock 可以创建多个 Condition,实现更精细的线程协作控制
三、ReentrantLock 整体架构
ReentrantLock 的实现并不复杂
核心结构如下:
ReentrantLock
│
▼
Sync
│
┌────┴────┐
▼ ▼
FairSync NonfairSync
│
▼
AQS
源码:
public class ReentrantLock implements Lock {
private final Sync sync;
}
其中:
abstract static class Sync
extends AbstractQueuedSynchronizer {
}
可以看到:
ReentrantLock 本身并不负责具体的同步逻辑
真正的加锁、解锁、线程排队等操作,全部由 AQS 完成
ReentrantLock 只是对 AQS 的一次具体实现
四、可重入锁实现原理
可重入的核心在于两个变量:
private volatile int state;
以及:
private transient Thread exclusiveOwnerThread;
其中:
state 表示锁的重入次数
exclusiveOwnerThread 表示当前持有锁的线程
第一次获取锁
lock.lock();
执行后:
state = 1
owner = Thread-A
第二次获取锁
lock.lock();
发现:owner == 当前线程,因此无需竞争锁
直接:state++
变成:state = 2
第三次获取锁
state = 3
依此类推
释放锁
lock.unlock();
本质上执行:
state--
例如:
state = 3
第一次:state = 2
第二次:state = 1
第三次:state = 0
只有 state 变成 0 时,锁才真正释放
因此:
lock几次
必须unlock几次
否则会导致其他线程永远无法获得锁
五、公平锁与非公平锁实现原理
ReentrantLock 内部存在两个实现:
FairSync
和
NonfairSync
非公平锁
默认使用:
new ReentrantLock();
其加锁逻辑非常直接:
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
}
只要发现锁空闲,立即 CAS 抢占
即使同步队列中已经有等待线程,也允许直接插队
因此性能较高
公平锁
new ReentrantLock(true);
获取锁前会先判断:
hasQueuedPredecessors()
检查自己前面是否存在等待线程
如果存在:必须排队
不能直接获取锁
因此实现了严格的 FIFO 获取顺序
六、加锁与解锁流程
加锁流程
lock()
│
▼
tryAcquire()
│
▼
CAS(state)
│
├──成功
│
└──失败
│
▼
进入AQS队列
如果锁空闲:
state=0 → 1
直接获取成功
否则进入同步队列等待
解锁流程
unlock()
│
▼
tryRelease()
│
▼
state--
│
▼
state==0
│
▼
唤醒后继节点
当重入次数归零时:
exclusiveOwnerThread = null;
锁被真正释放
随后唤醒同步队列中的下一个线程继续竞争锁
七、ReentrantLock 与 synchronized 对比
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | Monitor | AQS |
| 可重入 | √ | √ |
| 公平锁 | × | √ |
| tryLock | × | √ |
| 超时获取锁 | × | √ |
| 可中断获取锁 | × | √ |
| 多条件队列 | × | √ |
| 自动释放锁 | √ | × |
从功能角度来看:
ReentrantLock 提供了比 synchronized 更丰富的并发控制能力
从性能角度来看:
JDK 1.6 之后 synchronized 引入偏向锁、轻量级锁等优化,两者性能差距已经非常小
因此:
-
简单同步场景优先使用 synchronized
-
复杂并发控制场景优先使用 ReentrantLock
更多推荐
所有评论(0)