在了解ReenTrantLock之前,我们首先需要理解一下为什么出现了这个工具。我们知道java官方给了我们一个Synchronized工具,但是在1.6之前,如果需要加锁的话就需要调用Linux系统的metex()函数,也就是切换到内核态,从用户态切换到内核态是比较费时间的一个事情。所以Doug Lea就写了ReentrantLock这个函数,不过其实java.util.concurrent这个包大部分都是他写的。这个函数可以在JVM的层面上去解决并发冲突的问题,而不用切换到内核态。不过随着JAVA版本的升级Synchronized关键字的性能也得到了很大的提升,在线程冲突比较严重的时候,反而Synchronized也会拥有不错的性能。
如果要理解ReenTrantLock,我们必须对它和AQS的结构有一个大概的认识:
ReenTrantLock:
ReenTrantLock有公平锁和非公平锁两种,在代码中它是这样实现的:
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer{....}
static final class NonfairSync extends Sync{....}
static final class FairSync extends Sync{....}
public ReentrantLock() {sync = new NonfairSync();}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
public void lock() {sync.lock();}
public void unlock() {sync.release(1);}
从这几个类和变量中可以看出,首先在ReenTrantLock中是使用sync来操作的,而根据构造参数的不同,选择不同的具体实现类。
AbstractQueuedSynchronizer:
AbstractQueuedSynchronizer中有几个主要的内容:
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
static final class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
int ws;
}
他们分别代表了队头,队尾,锁状态(加锁状态则为1,重入+1,解锁状态则为0),Node中的ws意为waitStatus,是一个状态标识;ws是一个过渡状态,在不同方法里面判断ws的状态做不同的处理。他们看起来像这样:
如果要分析ReentrantLock的话,我们来带入场景,以公平锁为例。首先只有一个线程t1,它调用了ReentrantLock对象的lock方法,这个时候我们来看看发生了什么:
1.首先调用sync的lock方法,
public void lock() {
sync.lock();
}
2.sync的lock方法如下
final void lock() {
acquire(1);
}
3.acquire()是AQS实现的方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire的意思类似于将,AQS中的state变为传入的参数。tryAcquire函数将尝试去修改,如果修改失败则执行入队操作(acquireQueued()),tryAcquire()是AQS要求子类实现的方法,我们看看公平锁中是如何实现的:
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;
}
首先我们直接查看state状态,这个时候只有t1来进行lock,所以state为0,hasQueuedPredecessors方法判断队列是否被初始化,如果没有初始化显然不需要排队,我们可以来看看它的源码:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
如果队列没有被初始化过,tail=head=null,hasQueuedPredecessors返回false。之后tryAcquire将使用CAS设置state为传入的参数,如果设置成功,则将AQS中的持有锁线程设置为本线程并返回true,那么acquire也将得以返回,之后lock正常执行。这就是只有一个线程或者多个线程交替执行的情况,所以你可以看到在线程冲突不严重的情况下,lock的过程对性能影响非常小。如果是1.6之前版本的Synchronized每次加锁都要进入内核态,对性能影响非常大。现在我们来总结一下这第一个获得锁的线程都干了什么,首先设置了state,并将AQS中的持有锁线程设置为本线程,AQS中的队列依旧没有初始化。
如果t1没有释放锁,这个时候有来了t2,再来看看lock将如何执行(公平锁):
从上面的代码可以看到,一直到tryAcquire()都是和t1一样的,但是在tryAcquire()中将会执行:
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
它判断持有锁线程是否是当前线程,如果是则将state设置为state+acquires,这也体现了ReentrantLock是可重入锁。如果不是tryAcquire返回false;
这个时候再来看acquire将会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg));
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
它首先执行addWaiter:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
他首先实例化一个node,这个时候tail和head都是null,所以pred为null,执行enq(node):
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
这个时候tail还是为null的,所以t == null;之后执行compareAndSetHead(new Node())
将Head指向一个空节点,并且将tail也指向这个空节点。他看起来像这样:
完成之后再次循环,这一次 t != null执行else内容,之后变成:
请务必记住一个原则在AQS的队列中第一个节点的值永远是null,之后enq返回,addWaiter返回,执行acquire中的acquireQueued():
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个函数将会取出node的前一个节点赋值给p,如果p==head,则表示当前节点是第一个排队的线程,他会再次尝试获取锁,这个时候,如果t1还是没有释放锁,则获取锁失败,t2执行shouldParkAfterFailedAcquire
,注意看这个函数的名字,可能停止在获取锁失败之后。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
当前节点的前一个节点的waitStatus被赋值给ws,它至始至终都没有被赋值,所以为初始化的时候的值0,而Node.SIGNAL为-1;所以执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
它把前一个节点的waitStatus设置为-1;之后返回false;回到acquireQueued,再次执行其中的for(;;)
这个线程又一次去获取锁,如果又获取失败,再次进入shouldParkAfterFailedAcquire
这一次返回的是true。所以回到acquireQueued
之后执行:parkAndCheckInterrupt();
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
LockSupport.park(this)
;将会阻塞,直到被唤醒,继续执行。
现在总结一下,t2获取AQS状态值,发现不等于0,则直接入队,这时候队里有一个值为空的节点,和值为t2的节点。之后t2两次尝试去获取锁,失败后被park;
这个时候又来了一个t3,直到addWaiter()
之前t3的执行逻辑和t2一样,在addWaiter()
中pred != null;执行其中的:
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
将t3直接入队,返回执行acquireQueued
:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这个时候t3节点的前一个节点不等于head。直接执行shouldParkAfterFailedAcquire
,由于前一个t2节点的waitStatus也从来都没有被设置过,所以初始值为0,所以执行shouldParkAfterFailedAcquire
函数中的compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
并返回false,再次循环,这次返回true,进入休眠。
这里需要注意waitStatus
的状态,可以想象,在AQS的队列中,第一个值为空节点的waitStatus = -1
;t2节点的waitStatus = -1
;t3节点的waitStatus = 0
;
这里我们来复习一下t3入队做了什么,获取锁失败,直接入队,然后设置前面一个节点waitStatus = -1
,休眠。
但是以上是t1一直持有锁的情况,下面我们来看看在t2,t3获取锁的时候,t1释放了锁的情况,让我们回到tryAcquire
代码:
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;
}
无论t1是否释放锁,其他线程想要获得锁,必须执行tryAcquire
,如果持有锁线程释放了锁,那么c == 0,我们会去执行hasQueuedPredecessors
:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
之前只有t2调用lock的时候,我们才会去执行hasQueuedPredecessors
,那个时候队列还没有被初始化,直接返回false,但是现在不同了,任何情况,只要c == 0,就有可能执行hasQueuedPredecessors
。
我们先来回忆一下,当什么时候tryAcquire
代码会被调用呢?首先是每一个线程执行lock函数的时候,调用1次。第一个需要排队的线程入队后需要调用2次,第一个排队的线程被唤醒的时候(持有锁的线程unlock的时候,unpack第一个在排队的线程,第一个排队的线程得以被唤醒),继续调用tryAcquire
,关于这一点你可以回忆一下下面这个函数:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
第一个排队的线程在parkAndCheckInterrupt())
这里被阻塞,唤醒后继续执行 if (p == head && tryAcquire(arg))
中的tryAcquire(arg)
。
每次调用tryAcquire(arg)
的时候,都有可能锁被释放,也就是c == 0
从而进入hasQueuedPredecessors
,从注释中我们可以看到,这个方法返回false,在队列没有被初始化或者当前线程是队列中第一个在等待的线程的情况下。Predecessors意味前驱,他实际上是判断有没有线程比当前线程等待的久的线程。注释中也给出了这个解释:
Queries whether any threads have been waiting to acquire longer than the current thread.
实际上你也可想象的到,这个函数是为公平锁所设计的,非公平锁看到锁是释放的,直接加锁,根本不管前面有没有线程等的比它久,你可在ReentrantLock中看到非公平锁的TryAcquire实现如下,印证了这个说法:
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;
}
最后我想说说ReentrantLock中的interrupted
在acquireQueued
函数中有很多interrupted
:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
不过在了解interrupted
之前,我们先来看看interrupted怎么用,
- interrupt():中断当前线程,实际上它没有任何动作,只是设置一下一个线程标记
- interrupted():查询当前线程中断状态,中断状态则返回true,否则返回false,并且清除这个标记,也就是无论之前是什么,现在置为false;
在线程执行parkAndCheckInterrupt()
休眠的过程中,被中断,醒来后Thread.interrupted()
返回为true,并且将线程中断置为false,注意这个置为false的行为,这意味着用户设置的线程中断状态将不再生效,如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
返回后acquireQueued
函数中的interrupted
被置为true,线程获取锁后将返回给acquire
这个true,acquire
将调用selfInterrupt()
函数。将线程中断标志位再次设置为true。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
到现在看来,你肯定很迷惑如果parkAndCheckInterrupt
中不调用Thread.interrupted()
不就可以没有其余的这些步骤了吗了吗?这也是迷惑很多人的一点,但是如果你知道lock的lockInterruptibly();
函数你就会知道Doug Lea为什么这样做,我们来对比一下lockInterruptibly()
和lock()
调用流程的区别:
lock() -> sync.lock() -> acquire() -> acquireQueued() -> parkAndCheckInterrupt()
lockInterruptibly() -> sync.acquireInterruptibly() -> doAcquireInterruptibly() -> parkAndCheckInterrupt()
可以看到最后这两个方法都调用了 parkAndCheckInterrupt()
这个方法,而在lockInterruptibly()
的调用中parkAndCheckInterrupt()
是有意义的,
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
你可以看到,在doAcquireInterruptibly
中如果parkAndCheckInterrupt
显示线程在队列等待的过程中被中断过,则会抛出InterruptedException();
只是在lock()中parkAndCheckInterrupt
返回值是没有意义的,反而有副作用(将用户设置的中断状态清除)。所以现在你也知道了,如果在执行lock()和lockInterruptibly() 过程中如果线程被中断了会如何。
以上就是ReentrantLock的部分内容,如果读者有兴趣,可以继续分析ReentrantLock()和AQS源码中的内容,相信你一定会敬佩Doug Lea这位大神强大的编码技术。
参考内容:https://www.bilibili.com/video/av67194367?p=3