一、前言
我们上一篇分析了 AQS 《小白十二》,重点讲了获取锁和释放锁的流程,AQS 是抽象类,本篇我们就来聊聊 AQS 的子类:重入锁。再正式聊重入锁前,我先提几个小问题:
- 为什么会有重入锁?
- 重入锁使用的场景?
- 没有重入锁会发生什么结果?
好了,带着这几个问题,我们开始我们的重入锁之旅。
二、不可重入锁(自己实现)
JDK 中我并没有找到不可重入锁的源码,因此,只好我们手动来实现一个,看看锁不可重入的结果:
// NonReentrantLock.java
public class NonReentrantLock {
private final AtomicReference<Thread> reference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
while (!reference.compareAndSet(null, thread)); // CAS 加锁
}
public void unlock() {
Thread thread = Thread.currentThread();
while (!reference.compareAndSet(thread, null)); // CAS 解锁
}
}
测试代码:
public class Demo implements Runnable {
private NonReentrantLock lock = new NonReentrantLock();
public static void main(String[] args) {
new Thread(new Demo()).start();
}
@Override
public void run() {
int res = fab(3);
System.out.println("3! = " + res);
}
private int fab(int n) {
System.out.println("Enter in fab............");
lock.lock();
try {
if (n <= 1) {
return 1;
}
return n * fab(n - 1);
} finally {
lock.unlock();
System.out.println("Exit in fab............");
}
}
}
输出结果:
Enter in fab............
Enter in fab............
我们来分析下造成这个结果的原因:
- 第一次调用 fab 方法(传入数值3),进入并上锁;
- 第二次调用 fab 方法(传入数值2 = 3-1),尝试上锁,由于锁已经被持有,只能CAS原地等待;
- 因为无法拿到锁,也就无法继续执行;
结果是:死锁!
同时,我们还需要注意一点:
- Demo 中是同一个线程;
- 同一个线程多次进入资源临界区;
因此,不可重入锁对于任何线程,无论是同一个线程,还是不同线程,都一视同仁,没有区别对待,那么,当存在递归时,将会发生死锁;以上的例子非常简单,我们可以想办法来避免,但实际开发中,我们很难保证同一个线程不会二次进入资源临界区而发生死锁,因此,这也是我开头提的三个小问题的解答。
三、可重入锁(自己实现)
锁通常是针对于不同线程,对同一块资源竞争时的手段,然而,当同一线程在已持有锁的情况下,多次进入同一资源时,我们就当能够识别出来,并让其进入;但同时,我们也需要做好记录,记录持有锁的线程进入次数与退出次数,要保证进入与退出次数正好相等时,最后一次的退出才应该真正的释放锁;否则,同样也会产生死锁。
我们修改上面的例子,为了不影响之前的 demo,我们新建个类:
// CanReentrantLock.java
public class CanReentrantLock {
private static final class LockObject {
Thread thread = null;
int count = 0;
LockObject(Thread thread) {
this.thread = thread;
this.count = 1;
}
}
private final AtomicReference<LockObject> reference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
LockObject object = reference.get();
if (object == null) {
object = new LockObject(thread);
while (!reference.compareAndSet(null, object));
} else if (object.thread == thread ){
object.count ++;
reference.set(object);
} else {
while (!reference.compareAndSet(null, new LockObject(thread)));
}
}
public void unlock() {
Thread thread = Thread.currentThread();
LockObject object = reference.get();
if (object != null && object.thread == thread) {
object.count --;
if (object.count == 0) {
while (!reference.compareAndSet(object, null)) ;
}
}
}
}
修改我们的测试类:
将下面的这行代码
private NonReentrantLock lock = new NonReentrantLock();
替换成
private CanReentrantLock lock = new CanReentrantLock();
即可
我们再次执行我们的测试用例,结果如下:
Enter in fab............
Enter in fab............
Enter in fab............
Exit in fab............
Exit in fab............
Exit in fab............
3! = 6
结果符合我们的预期!
四、JDK 之 ReentrantLock
ReentrantLock 就是 JDK 提供的重入锁,它有一个内部静态抽象类(Sync),继承于 AQS;同时,它还有两个静态实现类继承于 Sync,分别是:NonfairSync(非公平锁)和 FairSync(公平锁)。ReentrantLock 是独占模式,因此,Sync及其子类,需要实现三个方法:
- isHeldExclusively:根据请求的线程与当前持有的线程进行比对来判断是否需要独占,这是重入的机制(Sync实现);
- tryRelease:释放锁(Sync实现);
- tryAcquire:获取锁(公平与非公平在此处实现有差异,后续会分析);
4.1、构造函数
public class ReentrantLock implements Lock, java.io.Serializable {
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
默认构造函数使用的是非公平锁,我们也可以指定使用公平锁。
4.2、公平锁(FairSync)
public class ReentrantLock implements Lock, java.io.Serializable {
static final class FairSync extends Sync {
/**
* 回忆一下 AQS 的获取锁流程,再结合具体的子类,整个获取锁就能串起来:
* lock -> acquire -> tryAcquire
*/
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 资源占用情况:0 = 未占用;1 = 占用
int c = getState();
if (c == 0) {
// hasQueuedPredecessors 判断 CLH 是否为空,这就体现出『公平』特性
// 如果 CLH 不为空那么就不会去竞争抢占锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// CLH 为空,且 CAS 成功,将 owner 设置为当前线程
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;
}
}
}
公平锁比较简单,我们来看下,非公平锁的实现。
4.3、非公平锁
public class ReentrantLock implements Lock, java.io.Serializable {
static final class NonfairSync extends Sync {
// 非公平锁,在一开始 lock 时,就先 CAS 尝试一次,如果失败则调用 acquire
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// lock 失败 -> acquire -> tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
}
我们就目前看到,非公平锁在加入 CLH 之前,利用尽可能的『机会』去尝试获取锁:
- lock:第一次;
- tryAcquire:虽然我们没看到实现,但是从调用的方法名上就能看出"非公平再尝试";
4.3.1、Sync.nonfairTryAcquire
public class ReentrantLock implements Lock, java.io.Serializable {
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)) {
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;
}
......
}
}
请大家仔细对比一下 FairSync.tryAcquire 方法与 Sync.nonfairTryAcquire 方法,你会发现,只有一行代码的区别:
if (c == 0) {
if (!hasQueuedPredecessors() && // 公平与非公平,唯一的区别
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
公平锁多了一个 CLH 的判断,而非公平锁再次 CAS 尝试获取锁,而无视 CLH。
4.4、静态内部抽象类Sync
public class ReentrantLock implements Lock, java.io.Serializable {
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
......
}
// 释放锁;判断是否有多次重入,直到计数为0,才释放锁
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;
}
// 独占模式,且判断是否是:重入
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
}
}
五、总结
正因为我们详细分析了 AQS 的获取和释放锁的流程;再加上,我也用自己实现的一个例子来前后对比不可重入锁的后果;因此,我们学习 ReentrantLock 非常的轻松;至少 ReentrantLock 中的其它代码,仅仅只是 setter / getter ,并不影响核心流程;因为锁的释放比较简单,因此我就不在过多分析了,大家自己看一下就清楚了。
网友评论