文章目录
- 前言
- 正文
- 一、Lock接口的定义
- 二、ReentrantLock 的实现
- 三、AbstractQueuedSynchronizer的实现
- 3.1 AQS 中的加锁底层
- 3.2 ReentrantLock中的 Sync 同步器
- 3.3 NonfairSync 的实现
- 3.4 FairSync 的实现
- 3.5 公平锁和非公平锁的总结
- 3.5.1 公平锁
- 3.5.2 非公平锁
- 3.6 释放锁
前言
提起Java中的锁,一般我们最快的反应是 synchronized
。但是在Java1.5之后,Doug Lea 大姥设计并实现的 JUC 中,提供了更加丰富的API操作。其中 Lock
接口及其相关实现尤为经典。今天我们来一起学习这 JUC 中的优秀设计思想。
与synchronized
有锁不同,Lock
接口及其实现是由Java代码实现的。其底层代码实现,基于抽象队列同步器(AbstractQueuedSynchronizer
)以及 volatile
关键字、CAS机制。
以下内容分析,代码参考于Java11。
正文
一、Lock接口的定义
因为Lock
本身是一个接口,所以我们在用的时候,基本都是找它的实现。而我们经常用到的是ReentrantLock
类。Lock中定义的方法如下:
对应的官方文档如下:Lock接口官方文档
从文档中也可以得知,经典的Lock用法是:
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// 上锁
lock.lock();
try {
// TODO 你的需要加锁的代码
} finally {
// 解锁
lock.unlock();
}
}
需要额外注意的是, Lock在使用时,需要手动加锁和释放锁,其中释放锁需要放在 try-finally 代码块中的 finally 块内,保证其能及时释放。
二、ReentrantLock 的实现
ReentrantLock
是 Lock
接口的一个实现类 。也是我们经常用的一个锁,下面我们从源码的角度来看看它是如何实现一个锁的功能的。
可以先来看看类之间的关系:
随后我们看看ReentrantLock类的构造器是怎样的。
它一共提供了2个构造器:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
关键的加锁和释放锁的方法也很短小:
// 加锁
public void lock() {
sync.acquire(1);
}
// 释放锁
public void unlock() {
sync.release(1);
}
观察构造器以及加锁、释放锁的方法我们可以知道,默认情况下我们使用的锁都是基于 NonfairSync
(不公平同步器)的。
当你传入参数,一个布尔值,可以进行选择,使用公平还是不公平的同步器。
而在加锁、释放锁时调用的方法,是在抽象队列同步器(AbstractQueuedSynchronizer
)中实现的。也就是常常被人提到的得 AQS
。
三、AbstractQueuedSynchronizer的实现
3.1 AQS 中的加锁底层
通过第二小节的分析,我们能够知道,加锁调用了acquire方法。在AQS中的定义如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 创建新节点,并将新节点放在队列尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 补偿机制,自我中断
selfInterrupt();
}
在这段代码中,我们看到会调用一次 tryAcquire
方法(尝试获取锁):
-
如果该方法返回true会直接结束;
-
如果该方法返回false,会调用
acquireQueued
,给队列中增加节点;增加节点返回true时,会中断当前线程。 -
acquireQueued
内部是一个“死循环”,一直尝试调用tryAcquire
方法,而它只有在线程等待时中断或者出现其他异常时,会返回true。-
final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; try { for (;;) { // 拿到node的前一个节点 final Node p = node.predecessor(); // 若前一个节点是head,说明自己现在是第一个,可以尝试获取锁 if (p == head && tryAcquire(arg)) { // 将本次节点设置为head setHead(node); // help GC p.next = null; // 获取到锁,返回false return interrupted; } // 阻塞判断:应该阻塞时会阻塞,不该阻塞时会再给一次抢锁机会 // 返回true表示需要阻塞 if (shouldParkAfterFailedAcquire(p, node)) // 阻塞线程,interrupted设置为true interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { // 取消获取锁 cancelAcquire(node); // 中断线程 if (interrupted) selfInterrupt(); throw t; } }
-
因此我们可以分析出来,tryAcquire
如果返回true就说明获取锁成功。
AQS 在设计这里的时候,使用了模板方法设计模式,将 tryAcquire
定义了,但是实现时只抛出了 UnsupportedOperationException
。
所以我们接下来要去看看AQS的子类(以及孙子类),也就是 Sync
、NonfairSync
、FairSync
。
3.2 ReentrantLock中的 Sync 同步器
ReentrantLock 中定义了Sync
、NonfairSync
、FairSync
。
3.3 NonfairSync 的实现
我们本小节主要关注“不公平同步器”,也是很常用的一种。
NonfairSync
中代码很少,如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
而它里边的 nonfairTryAcquire
方法在 Sync
中做了实现。
因此加锁方法的核心实现是:
// 这里的入参acquires是1
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取AQS中的 volatile 修饰的 state值,默认为0,表示没有获取锁
int c = getState();
if (c == 0) {
// 进行CAS改值,改值成功返回true,表示获取锁成功
if (compareAndSetState(0, acquires)) {
// 设置当前线程独占锁
setExclusiveOwnerThread(current);
return true;
}
}
// state值不是0,表示已经有线程持有锁
// 判断当前持有锁的是不是当前线程,如果是,则计算新的state值,获取锁成功
else if (current == getExclusiveOwnerThread()) {
// 计算新的状态值
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 修改AQS中的state
setState(nextc);
return true;
}
// 其他情况
return false;
}
基本流程如下:
3.4 FairSync 的实现
接下来我们看看公平锁时怎么做的,它和非公平锁又有什么区别呢。
FairSync
中实现了 tryAcquire
方法,如下:
@ReservedStackAccess
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;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
查看代码可以发现,它和非公平锁很像,只是在**“没有线程占用锁时,获取锁增加了额外的条件”**,这个条件是hasQueuedPredecessors
。它的含义是:查询是否有任何线程等待获取的时间超过当前线程。
也就是说,如果存在等待时间超过当前线程的线程,本轮流程中,当前线程就不会获取锁,会创建节点加入到队列中,看着挺公平的(不是绝对公平的)。以等待时间做为了判定条件。
有了这个等待时间的判定条件,我们就可以预料到一些问题:
假如队列里有等待时间不一的线程节点,有个长时间的任务,还有一些短时间的任务,那使用公平锁时,会优先给到长时间的那个任务,导致后边短时间可以执行完的任务一直在等。
也正是因此,它只是适合某种场景。我们默认使用时,大多还是会选择非公平锁。
3.5 公平锁和非公平锁的总结
这里省略了关于等待时间细节判断,以整个加锁流程来总结:
3.5.1 公平锁
在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会从尾结点加入队列。
3.5.2 非公平锁
在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁,这里其实是一种插队的表现。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。
此时,队列中被唤醒的线程并不一定能优先获得锁,当队列中被唤醒的线程被新来的线程抢占了资源,这种插队也就表现出了非公平的特性。
3.6 释放锁
在AQS中定义了 release
方法,用于帮助实现释放锁。这里的参数在ReentrantLock的unlock方法中传递了1。
public final boolean release(int arg) {
// 尝试释放锁,true表示释放成功
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒下一个节点(线程)
unparkSuccessor(h);
return true;
}
return false;
}
在ReentrantLock
的内部类中的Sync
对 tryRelease 方法进行了重写。
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
// 当前state减去1
int c = getState() - releases;
// 当前线程不是占有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 释放成功
if (c == 0) {
free = true;
// 设置没有线程占用锁了
setExclusiveOwnerThread(null);
}
// 修改state值
setState(c);
return free;
}