目录
- ReentrantLock 简介
- ReentrantLock 使用示例
- ReentrantLock 与 synchronized 的区别
- ReentrantLock 实现原理
- ReentrantLock 源码解析
ReentrantLock 简介
ReentrantLock 是 JDK 提供的一个可重入的独占锁,
- 独占锁:同一时间只有一个线程可以持有锁
- 可重入:持有锁的线程可重复加锁,意味着第一次成功加锁时,需要记录锁的持有者为当前线程,后续再次加锁时,可以识别当前线程。
ReentrantLock 提供了公平锁以及非公平锁两种模式,要解释这两种模式不异同,得先了解一下 ReentrantLock 的加锁流程,ReentrantLock 基于 AQS 同步器实现加解锁,基本的实现流程为:
线程 A、B、C 同时执行加锁,加锁是通过CAS操作完成,CAS 是原子操作,可以保证同一时间只有一个线程加锁成功,假设线程 A 加锁成功,则线程 B、C 进入 AQS 等待队列并被挂起,假设 B 在前,C 在后,当线程 A 释放锁时,会唤醒排在等待队列队首的线程 B,该线程会尝试通过 CAS 进行获取锁。如果线程 B 尝试加锁的同时,有线程 D 也同时进行加锁,如果线程 D 与 线程 B 竞争加锁,则为非公平锁,线程 D 加入等待队列排在线程 C 之后,则为公平锁。
- 非公平锁:加锁时会与等待队列中的头节点进行竞争。
- 公平锁:加锁时首先判断等待队列中是否有线程在排队,如果没有则参与竞争锁,如果有则排队等待。
所谓公平就是,大家一起到,就竞争上岗,如果已经有人在排队了,那就先来后到。
使用示例
使用伪代码表示
class X {
private final ReentrantLock lock = new ReentrantLock();
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock();
}
}
}
默认实现的是非公平锁,如果要使用公平锁,只需要创建 ReentrantLock 对象时传递入参 true 即可,使用方法与非公平锁一样。
private final ReentrantLock lock = new ReentrantLock(true);
condition 使用示例:
class X {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void poll() {
lock.lock(); // block until condition holds
try {
while(条件判断表达式) {
condition.wait();
}
} finally {
lock.unlock();
}
}
public void push() {
condition.signal();
}
}
ReentrantLock 与 synchronized 的区别
ReentrantLock 提供了 synchronized 类似的功能和内存语义。
相同点:
- ReentrantLock 与 synchronized 都是独占锁,可以让程序正确同步。
- ReentrantLock 与 synchronized 都可重入锁,可以在循环中使用 synchronized 进行加锁并不用担心解锁问题,但 ReentrantLock 则必须要进行与加锁相同次数的解锁操作,不然可能导致没有真正解锁成功。
不同点:
- synchronized 是JDK提供的语法,加锁解锁的过程是隐式的,用户不用手动操作,操作简单,但不够灵活。
- ReentrantLock 需要手动加锁解锁,且解锁次数必须与加锁次数一样,才能保证正确释放锁,操作较为复杂,但是因为是手动操作,所以可以应付复杂的并发场景。
- ReentrantLock 可以实现公平锁
- ReentrantLock 可以响应中断,使用 lockInterruptibly 方法进行加锁,可以在加锁过程中响应中断,synchronized 不能响应中断
- ReentrantLock 可以实现快速失败,使用 tryLock 方法进行加锁,如果不能加锁成功,会立即返回 false,而 synchronized 是阻塞式。
- ReentrantLock 可以结合 Condition 实现条件机制。
可以看到,ReentrantLock 与 synchronized 都是实现线程同步加锁,但 ReentrantLock 比起 synchronized 要灵活很多。
实现原理
ReentrantLock 使用组合的方式,通过继承 AQS 同步器实现线程同步。通过控制 AQS 同步器的同步状态 state 达到加锁解锁的效果,该状态默认为 0,代表锁未被占用,加锁则是通过 cas 操作将其设置为 1,cas 是原子性操作,可以保证同一时间只有一个线程可以加锁成功,同一个线程可以重复加锁,每次加锁同步状态自增 1,释放锁的过程就是将同步状态自减,减到 0 时才算完全释放,这也解释了为什么释放锁的次数必须与加锁次数一样的问题,因为只有次数一样才能将同步状态减至 0,这样其它线程才能进行加锁。
源码分析
Lock 接口
ReentrantLock 实现了 Lock 接口,这是 JDK 提供的所有 JVM 锁的基类。
public interface Lock {
// 阻塞式加锁
void lock();
// 阻塞式加锁,但可以响应中断,加锁过程中线程中断,抛出 InterruptedException 异常
void lockInterruptibly() throws InterruptedException;
// 快速失败加锁,只尝试一次,
boolean tryLock();
// 阻塞式加锁,可以响应中断并且实现超时失败
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 实现条件
Condition newCondition();
}
通过代码可以看到,ReentrantLock 的内部实现都是通过 Sync 这个类实现,可以认为遵守组合设计原则,Sync 是 ReentrantLock 的内部类。这里的方法调用,并没有区分是公平锁还是非公平锁,而是无差别地调用,所以区别一定在 Sync 这个类的实现中。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
}
Sync 类继承了 AQS 同步器,通过同步器实现线程同步,因为是独占锁,所以最重要的就是实现 tryAcquire
与 tryRelease
两个方法,Sync 类是一个 abstract 类,它拥有两个实现类 FairSync
与 NonfairSync
,通过名字应该就可分辨他们就是公平锁与非公平锁。
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
// 非公平加锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果没有线程执行锁
if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁
setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁
int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 将同步状态进行自减,acquires 的传值为 1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 当同步状态减成 0 时,代表完全释放锁,将锁的拥有者置空
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// ...省略其它...
}
所以公平锁与非公平锁的玄机就在 ReentrantLock 的构造方法中,默认的无参构造方法创建非公平锁,如果传参 true,则创建公平锁。而这两个锁都是 Sync 的子类,使用了不同的实现策略,可以认为使用了策略模式。
public class ReentrantLock implements Lock, java.io.Serializable {
// 默认创建非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 如果为 true,则创建公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
接下来分别看一下 FairSync
与 NonfairSync
是如何实现公平锁与非公平锁的,首先分析非公平锁
static final class NonfairSync extends Sync {
// 阻塞式加锁
final void lock() {
// 首先尝试竞争加锁,如果成功则设置当前线程为锁的拥有者
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 使用 AQS 排队
}
// 尝试加锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
阻塞式加锁调用 ReentrantLock 的 lock()
方法,该方法调用 sync.lock()
执行加锁,非公平锁也就是调用 NonfairSync
类的 lock
方法,该方法首先尝试竞争加锁,此时有三种情况:
- 此时锁没有人持有,竞争成功,直接设置当前线程为锁的拥有者并返回
- 此时锁没有人持有,竞争失败,走 AQS 加锁流程
- 此时锁被其它线程拥有,走 AQS 加锁流程
- 此时锁被自己拥有,竞争失败,走 AQS 加锁流程
AQS 加锁流程就是调用 tryAcquire 方法尝试加锁,如果成功则返回加锁成功,如果失败则进入等待队列并挂起,等待锁的持有者释放锁时唤醒等待队列中的线程,并再次尝试加锁,如此反复,直到加锁成功。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS 加锁流程在 AQS 中已经提供了完整实现模板,不需要去了解底层就可以使用,需要做的就是自行实现 tryAcquire 方法,NonfairSync 的 tryAcquire 方法这里再贴一次实现代码。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果没有线程执行锁
if (compareAndSetState(0, acquires)) { // 通过 CAS 尝试加锁
setExclusiveOwnerThread(current); // 加锁成功,设置锁的拥有者为当前线程
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程占有,说明是重复加锁
int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
接下来看一下 FairSync 的实现
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果锁没有人持有
// 首先判断队列是否为空,如果为空则竞争锁,如果不为空则返回尝试失败,线程会被加入等待队列
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 如果锁已经被当前线程持有,与非公平锁同样处理
int nextc = c + acquires; // 将同步状态进行自增,acquires 的传值为 1
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
释放锁的过程,公平锁与非公平锁是一样的,前面的代码中已经解释过了,这里就不再多说了。