前言
注:本文的源码来自 JDK11
ReentrantLock
是 Java 中的一个可重入锁,它可以用于替代传统的 synchronized
关键字来进行线程同步。
下面是与 synchronized
关键字的一些对比:
名称 | 实现 | 重入性 | 中断性 | 公平性 | 是否支持超时 | 释放锁 |
---|---|---|---|---|---|---|
ReentrantLock | Java API级别,基于AQS实现 | 可重入 | 可中断 | 支持非公平与公平 | 可以超时 | 手动是否锁 |
synchronized | JVM 级别,基于对象的 monitor 对象实现 | 可重入 | 不可中断 | 非公平 | 无法设置超时 | 自动是否锁 |
可以看出 ReentrantLock
提供了更多的灵活性和可扩展性,不知你是否开始对它的原理产生兴趣?
注意:你需要对 AQS 的工作原理有所了解,因为 ReentrantLock 是在 AQS 的基础上实现的。
类结构
ReentrantLock
类实现了 java.util.concurrent.locks.Lock 接口,它的内部实现包括一个 Sync
内部类,该类是ReentrantLock
的核心实现。
Sync
继承了 AbstractQueuedSynchronizer
抽象类,用于管理线程的同步状态。Sync
类有两个子类,分别是NonfairSync
和 FairSync
,用于实现非公平锁和公平锁。UML 如下图所示:
Sync 类源码
由于公平锁与非公平锁是 Sync
的子类,那么我们先分析 Sync
类
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* 默认的非公平实现,有个细节是,为什么非公平的实现要放父类这里?
* 其实 ReentrantLock 的 tryLock() 就是直接调这里的,不管你是哪个实现,都是使用非公平的实现。
*/
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果 state 为 0 表示没有线程占用锁
if (c == 0) {
// cas 成功表示拿到锁
if (compareAndSetState(0, acquires)) {
// 设置当前获取锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 因为是可重入锁,当 state 不为 0 时,再判断下如果为当前线程,则将 state 加上去,释放锁的时候再减
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 从这里可以知道,可重入次数是有限制的,即为 2147483647
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 由于是自己线程,不可能存在竞争关系,没必要用 cas
setState(nextc);
return true;
}
return false;
}
/**
* 释放锁的逻辑,非公平与公平的都是一样的,因为已经获取到锁,因此这里的代码都线程安全的,都不用 cas
*/
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// 略...
}
FairSync(公平锁源码)
加锁
下面先来看公平锁的加锁逻辑,这里只需实现 tryAcquire
,因为 父类 Sync
已经实现了 tryRelease
方法。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 公平锁的 tryAcquire 方法,这里跟非公平的实现几乎一样,只不过多了 hasQueuedPredecessors() 的判断
*/
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors() 表示在当前线程之前,队列里是否还有线程等待
// true: head --> node(thread1) --> node(currenThread)
// false: head --> node(currenThreaad)
// 因为必须保证公平,所以只需要按照 AQS 的 FIFO 队列来就好了,当前线程没有在队头就获取不到锁返回 false
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
解锁
解锁直接使用父类的,因此不用重写。
NonfairSync(非公平锁源码)
加锁
下面先来看非公平锁的加锁逻辑,非常的简洁。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
protected final boolean tryAcquire(int acquires) {
// 直接使用父类的方法,在上面已经分析过了,请拉回去看看
return nonfairTryAcquire(acquires);
}
}
解锁
同样的解锁直接使用父类的,也不用重写。
为什么非公平锁的效率比公平锁的高?
很多时候都不推荐使用公平锁的方式去加锁,都说使用非公平锁的效率要比公平锁的要好,但是为什么呢?
从上面的源码来看,公平锁也仅仅是多了 hasQueuedPredecessors()
的判断,其实在唤醒队列中阻塞的线程时涉及到上下文切换,这需要一定的时间消耗,多个线程的耗时累积起来效率自然就低了。
要尽量地减少这个上下文切换的时间也很简单,直接让当前的线程去抢锁,因为当前线程就在用户态,不会有上下文切换这个耗时,效率自然就好了。
总结
本文对比了 ReentrantLock
与 synchronized
的区别,知道 ReentrantLock
在用法上更加的灵活;分析了ReentrantLock
公平锁与非公平锁的实现,发现代码是非常简单的,这都得益于 AQS 这个抽象同步框架,其实它们的主要区别是在加锁的时候会不会尝试获取锁;最后思考了为何非公平锁的效率会比公平锁的高。