简介
ReentrantLock应该是我们最熟悉的锁。他是一个独占的且提供公平与非公平两种策略选择的锁。
它用AQS实现了一个内部类Sync,重写了tryRelease用来释放锁。同时Sync类有两个子类FairSync和NonfairSync,分别重写了tryAcquire用来实现获取锁的不同策略。接下来我们来看一下这两个子类。
NonfairSync
我们先来看一下非公平模式下的lock操作
final void lock() {
//直接尝试获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
我们可以看见,这里线程都会直接尝试获取锁,这也就是不公平的体现,因为之后我们会看到如果获取锁失败的线程都会进入队列进行等待并且被挂起。如果失败了,就调用acquire。acquire是AQS内部的一个操作流程。
public final void acquire(int arg) {
if (!tryAcquire(arg) && //调用同步器实现的tryAcquire方法
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //将当前线程加入到队列中
selfInterrupt();
}
acquire他会先去尝试获取锁(tryAcquire),这里就会调用NonfairSync中实现的方法了。如果失败了,则生成一个节点,并加入到等待队列中。
由于NonfairSync中的tryAcquire往下是直接调用了Sync中的nonfairTryAcquire方法,那就让我们直接看一下nonfairTryAcquire是怎么实现的。
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()) {
//可重入的体现,每重入一次状态值+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里可以看到,如果锁并未被任何线程占有,那么当前线程就会尝试获取一次锁,但是当锁已经被占用,那就会查看当前线程是否是占有锁的线程。如果是那就会把当前状态值+1,代表重入次数加一次,这也就是Reentrant实现可重入的原理。
到这里我们已经看到了非公平策略下ReentrantLock是如何实现tryAcquire来做到非公平策略以及可重入的了。总结一句话就是不管三七二十一,lock的时候都先尝试获取锁,失败了就进入队列,等待被唤起。
简易的流程图如下。
fairSync
看完非公平的实现,那么公平的实现应该可想而知。去掉非公平的部分,大家一起入队就能实现公平策略了。那么我们来看一fairSync中的tryAcquire和lock是如何实现的。
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;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
lock的实现变为直接调用acquire,而不是直接尝试获取锁。 tryAcquire则是会先调用hasQueuedPredecessors询问是否有前置节点,如果没有才开始尝试获取锁。
unlock流程
看完了如何加锁的,我们再来看一下如何释放锁。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) { //调用Sync中实现的tryRelease
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
可以看到,在ReentrantLock中公平和非公平策略下释放的方式都是一样的。 尝试释放锁,如果成功了,就会尝试唤起第一个节点。然后tryRelease实现如下
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;
}
tryRelease会将当前状态值-1代表重入数量-1,当状态值为0的时候才是真正释放了锁。
小结
ReentrantLock只实现了AQS的几个接口就是实现了他的性质,是不是很强大。简单概述一下实现这几个性质的手段吧。
- 独占:通过设置setExclusiveOwnerThread,获取锁的时候判断持有锁的线程是否是当前线程。
- 可重入:通过State这个状态值,状态值+1表示重入数+1
- 公平策略:当前去队伍排队一个个来
- 非公平策略:新来的线程都先试着获取一下,获取不到拉倒,排队去。
通过以上的这些技巧就可以构造一个简单的同步器,比如允许n个线程进行并发的同步器,以下是我实现的一个简单的这种同步器。
感谢您的阅读, 如果有什么地方写的有问题的,请麻烦指正一下,谢谢╰(°▽°)╯