背景
在之前的文章中提到了多线程并行操作在工程实践中的重要作用,也提到了如何保持临界资源和串行代码段的线程安全性是十分必要的。在计算机领域,是采用锁来保证线程安全性的。JAVA提供了多种锁的机制,比如早期的synchronized关键字,以它的简单易用被很多早期代码所采纳。但是后来开发者们发现,synchronized关键字没有提供很好的超时机制,且当多线程需要持有的锁相互依赖时容易导致死锁,因此后续JAVA提供了ReentrantLock来实现锁。
值得注意的是,JAVA推出ReentrantLock并不是用来直接替换掉synchronized关键字的,毕竟ReentrantLock需要在加锁操作后,进行解锁操作,如果仅进行了加锁而忘记了进行解锁,也是极为不正确的,会造成不良的后果。因此,ReentrantLock只是提供了一种多线程并发执行时,对临界资源进行加锁的新的思路和方法,至于在具体的使用场景中是应该使用synchronized关键字还是使用ReentrantLock,需要具体情况具体分析。
本文就来简单讲一下ReentrantLock是如何实现的。
ReentrantLock的常用方法及分析
ReentrantLock,顾名思义就是可重入锁。其主要方法如下:
- lock方法:加锁
- unlock方法:解锁
- tryLock方法:分为不带参数的tryLock方法和带有超时时间的tryLock方法;
- lockInterruptibly方法;
- newCondition方法:创建一个Condition,Condition维护了一个条件等待队列(注意不是线程同步队列)。
下边我们一一进行介绍。在介绍中,不可避免地会引入一个重要的类:AQS(AbstractQueuedSynchronizer),在ReentrantLock中,各重要方法的实现过程中都有它的介入。在AQS中维护了state字段(同步器的状态位)和线程同步队列。同时,在ReentrantLock中,当多线程尝试获取锁时,JAVA引入了两种方式:公平方式与不公平方式。公平方式是“先到先得”的方式,根据线程同步队列的排序依次尝试获取锁;非公平方式则与之相反,采用竞争的方式,不遵循线程同步队列的排序。
lock方法
lock方法主要用于加锁。
public void lock() {
sync.lock();
}
代码很简单,调用同步器,执行其lock方法。同步器设置部分在ReentrantLock的构造函数中,默认采用非公平的同步器--NonFairSync,原因在于,很多时候非公平的方式会比公平的方式性能更好。而FairSync和NonFairSync仅在尝试获取锁的时候有略微不同。
FairSync
我们先来看看FairSync的lock方法。
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其执行时序为:尝试获取锁,如果获取到了则直接返回;反之则将自身加入到同步队列中,即addWaiter,然后加入到同步队列中,等待被唤醒。
首先看一下尝试获取锁的部分,即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;
}
}
先获取当前状态state,当前线程能够获取锁的条件有两个:
- 当前没有其他线程持有锁,且没有等待获取该锁的线程(这条主要体现出公平性,即有人排在你前面,你就不能抢占锁);
- 当前虽然有线程持有锁,但是持有锁的线程恰好是当前线程本身(体现出可重入锁的“可重入特性”)。
对于第一种场景,采用CAS操作将state设置为输入的acquires,并将自身线程设置为持有锁的线程;
对于第二种场景,将state加上acquireds。
获取锁成功之后,lock方法就可以返回了。如果获取锁没有成功,则要进入线程同步队列并进行等待,也就是执行下述语句:
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这一语句其实包含了两层操作:先将当前线程包装成Node,然后加入到线程同步队列中;之后尝试将线程pack。
先来看加入线程同步队列的操作。addWaiter方法中,首先将当前线程包装成Node,只有包装成Node才能加入到后续的线程同步队列中。然后通过CAS操作,尝试设置到线程同步队列的尾部。如果CAS操作失败,也就是说同时还有其他线程也在执行入队操作,则执行enq操作,即在无限循环中,采用CAS操作将Node加到线程同步队列的尾部。
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;
}
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;
}
}
}
}
入队之后,尝试将当前线程pack。具体执行代码如下:
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);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
在死循环中,获取线程同步队列中当前节点的前置节点,如果前置节点已经是队列的头节点了,说明有可能当前节点可以获取锁了(之所以是可能,是因为有可能前置节点依然还持有锁,这样的话,当前节点依然要等待),那么就尝试获取锁,如果一切顺利,成功获取到锁,则将当前节点设置为队列的头节点,解除对原有头节点的链接关系(这样原有头节点就可以被后续的GC回收了),返回interrupt标志位。如果前置节点不是头结点,则考虑将当前节点对应的线程挂起(park)。挂起前需要先判断前置节点的状态,如果其为Node.CANCELED状态(值为1,即进行>0的判断分支),则将当前节点挂到它前边的非Node.CANCELED状态的节点之后;如果前置节点的状态为Node.SIGNAL状态,则当前节点对应的线程可以放心的挂起了,即对应return true的处理分支,然后交给LockSupport.park来将线程挂起;如果前置节点的状态是其他状态,则将其状态通过CAS操作改为Node.SIGNAL,然后交由上层死循环在下一次循环处理时,将当前线程放心挂起。
至此,FairLock的lock操作结束,等待unlock的时候将park的线程唤醒。
NonFairSync
接下来我们来简单看一下非公平的Sync。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
从代码中我们不难发现,由于是非公平的方式,因此线程直接采用CAS方式尝试获取锁,即将state从期望的0设置为1,而不用关心线程同步队列中是否还有排在它前边的线程。这是公平模式与非公平模式最大的差别。如果获取锁成功,则将当前线程设置为占有锁的线程。否则,与公平模式一样,尝试获取锁,获取不到则加入到线程同步队列中,然后将自己挂起,等待持有锁的线程释放锁之后将自己唤醒。
unlock方法
此方法用于释放锁,唤起线程等待队列中的线程。本方法对于公平模式与非公平模式没有不同。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这里我将调用链上的所有方法均粘贴在了一起。下边让我们来对此进行分析。
其实解锁操作与加锁操作相比,还是简单一些,因为它每一步都是与加锁操作相逆的。相对于加锁操作的获取当前同步器的status状态,并加上aquires,解锁操作也会获取当前同步器的status,并减去releases。接下来,如果获取锁失败,加锁操作会将当前线程加入到线程同步队列的尾部,并将自身挂起;相应地,解锁操作会获取线程同步队列的头部节点之后的第一个非Node.CANCELED状态的节点,将其对应的线程唤醒。
很多同学包括我自己看到这里会感觉到比较奇怪,为什么唤醒的不是头节点,而是头节点后的第二个节点呢?原因在lock方法调用链中的enq方法,当线程同步队列为空时,会新建一个节点作为头节点,让第一个等待的线程排在这个初始头节点的后边。在接下来的acquireQueued方法中,如下边所示,该线程如果不能获取到锁,则被park,一旦占有锁的线程释放了锁,该线程将被唤醒(注意此时它虽然是排位为1的等待线程,但却是线程同步队列中的head节点之后的第二个节点),然后进入第一个if分支,该线程对应的节点被设置成了头节点(也就是说,线程同步队列中,下一个被唤醒的线程一定是头节点之后的那个节点)。
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;
tryLock方法
不带参数的tryLock方法是即时返回结果的。如果该线程可以获取到锁(没有其他线程持有,即state=0;或者即时state!=0,但是持有该锁的线程为本线程),直接返回true,表示成功获取到锁;反之则返回false,表示没有成功获取到锁。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
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;
}
tryLock(long timeout, TimeUnit unit)
与不带参数的tryLock方法功能基本类似,唯一不同的是允许设置一个等待时间。
如果能成功获取到锁,则直接返回true;反之,则会在超时时间到达之际反馈是否成功获取到锁。超时机制是由方法doAcquireNanos实现的,当线程获取锁失败时,会进入此方法,在此方法中,timeout到来之前,park/unpark多次,检查是否能获取到锁,当timeout时间到来的时候,如果还没有获取到锁,则返回false。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
newCondition方法
此方法用于针对AQS同步器,创建一个Condition。如果说AQS主要功能在于维护一个线程同步队列,里边存储了等待持有锁的线程集合;那么Condition的作用在于维护一个条件等待队列,当调用Condition.await的时候,将线程持有的锁释放,放到条件等待队列中,等待通知;当调用signal方法的时候,将其从条件等待队列中去除,放置到线程同步队列中,等待被唤醒。
具体实现机制我会在后边的文章中专门加以介绍。