Lock
java.util.concurrent.lock
中的 Lock
框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock是什么?
ReentrantLock
类实现了 Lock
,它拥有与 synchronized
相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等的一些特性。当许多线程都想访问共享资源时,JVM可以花更少的时候来调度线程,把更多时间用在执行线程上,因此可以获得更好的性能。
AbstractQueuedSynchronizer
ReentrantLock
实现的前提就是AbstractQueuedSynchronizer
,简称AQS,是java.util.concurrent的核心,CountDownLatch
、FutureTask
、Semaphore
、ReentrantLock
等都有一个内部类是这个抽象类的子类。由于AQS是基于FIFO队列的实现,因此必然存在一个个节点,Node就是一个节点。
ReentrantLock使用场景
- 如果发现该操作已经在执行中则不再执行。
- 如果发现该操作已经在执行,等待一个一个执行。
- 如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行。
- 如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。
ReentrantLock实现原理
ReentrantLock中有一个抽象类Sync:
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
ReentrantLock根据传入构造方法的布尔型参数实例化出Sync的实现类FairSync和NonfairSync,分别表示公平的Sync和非公平的Sync。
这里顺带提一下公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。而非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
ReentrantLock非公平锁
lock()
由于ReentrantLock我们用的比较多的是非公平锁,所以看下非公平锁是如何实现的。
假设线程A调用了ReentrantLock的lock()方法,此时没有任何线程持有锁,那么线程A就持有了这个锁。调用链如下:
第一个获取锁的线程就做了两件事情:
- 设置AbstractQueuedSynchronizer的state为1
- 设置AbstractOwnableSynchronizer的thread为当前线程
这两步做完之后就表示线程1独占了锁。然后线程2也要尝试获取同一个锁,在线程1没有释放锁的情况下必然是行不通的,所以线程2就要阻塞。那么,线程2如何被阻塞?看下线程2的方法调用链,这就比较复杂了:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
线程2首先还是使用CAS尝试获取锁,如果state为0就将AbstractQueuedSynchronizer的state设置为1。因为线程1在持有锁的时候已经将state设置为1了,所以状态设置会失败。线程2执行acquire()
:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法做了两件事情:1.再次尝试获取一次锁,如果获取的结果为false即为失败,做第2件事情;2.添加FIFO等待队列。tryAcquire()
方法最后调用的是Sync的nonfairTryAcquire(int acquires)
方法。所以我们来看一下这个方法的执行情况:
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;
}
由于state是volatile的,所以state对于线程2是可见的。线程2再次尝试获取锁,有可能在线程2第一次获取锁失败到再次获取锁的过程中,线程1已经执行完代码将锁释放了。如果锁的状态不0,则判断线程2是否与正持有锁的进程是同一个。如果是,则将state+1,由于是重入,所以这里不会产生竞争,因此也不需要利用CAS设置state。从这个方法中我们可以了解到每次调用state都会加1,也就是nextc加1,当nextc<0的时候会抛出Error。也就是说同一个锁最多只能重入Integer.MAX_VALUE次(2147483647次)。
最后代码执行到AQS的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,模式为独占模式(Node.Exclusive == null)。
再判断一下此时队列上是否有节点,没有的话就创建一个等待队列,执行enq()
:
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,如果此时队列上没有任何节点,创建头结点head并将tail指向head。如果队列上存在节点,则将传进来的node.prev指向tail,并将tail.next指向node。
note:
enq方法采用的是变种CLH算法,先看头结点是否为空,如果为空就创建一个傀儡结点,头尾指针都指向这个傀儡结点,这一步只会在队列初始化时会执行;如果头结点非空,就采用CAS操作将当前结点插入到头结点后面,如果在插入的时候尾结点有变化,就将尾结点向后移动直到移动到最后一个结点为止,然后再把当前结点插入到尾结点后面,尾指针指向当前结点,入队成功。
由于线程2所在的Node是第一个要等待的Node,因此FIFO队列上肯定没有内容,tail为null,走的就是if条件通过的代码逻辑。这里用了CAS设置头Node,当然有可能线程2设置头Node的时候CPU切换了,线程3已经把头Node设置好了形成了上图所示的一个队列,这时线程2再循环一次获取tail,由于tail是volatile的,所以对线程2可见,线程2看见tail不为null,就走到了else里面去往尾Node后面添加自身。整个过程下来,形成了一个双向队列。最后走AQS的acquireQueued(node, 1):
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);
}
}
因为线程2是双向队列的真正的第一个Node(前面还有一个h),所以再次尝试一下线程2能不能获取锁(可能这段时间内线程1已经执行完了把锁释放了,state从1变为了0),如果还是获取失败,先调用AQS的shouldParkAfterFailedAcquire(p, 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;
}
该方法首先检查前趋结点的waitStatus位,如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了;
如果前趋结点是一个被取消的结点怎么办呢?那么就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止,将找到的这个结点作为它的前趋结点,将找到的这个结点的waitStatus位设置为SIGNAL,返回false表示线程不应该被挂起。
这个waitStatus是h的waitStatus,很明显是0,所以此时把h的waitStatus设置为Noed.SIGNAL即-1并返回false。既然返回了false,上面的acquireQueued的11行if自然不成立,再走一次for循环,还是先尝试获取锁,不成功,继续走shouldParkAfterFailedAcquire,此时waitStatus为-1,小于0,走第三行的判断,返回true。然后走acquireQueued的11行if的第二个判断条件parkAndCheckInterrupt:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
最后一步,调用LockSupport的park方法阻塞住了当前的线程。整个线程1独占锁,将线程2加入到FIFO队列并将其阻塞如上所述。
unlock()
调用ReentrantLock 的unLock()
方法:
public void unlock() {
sync.release(1);
}
调用AQS的release()
:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
执行Sync的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;
}
首先,只有当c==0的时候才会让free=true,这和上面一个线程多次调用lock方法累加state是对应的,调用了多少次的lock()方法自然必须调用同样次数的unlock()方法才行,这样才把一个锁给全部解开。
当一条线程对同一个ReentrantLock全部解锁之后,AQS的state自然就是0了,AbstractOwnableSynchronizer的exclusiveOwnerThread将被设置为null,这样就表示没有线程占有锁,方法返回true。
代码继续往下走,上面的release方法的第四行,h不为null成立(有等待队列),h的waitStatus不为0(h不为新节点)也成立,所以走第5行的unparkSuccessor方法:
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);
}
node参数传进来的是头结点,首先检查头结点的waitStatus位,如果为负,表示头结点还需要通知后继结点,这里不需要头结点去通知后继,因此将该标志位清0.
然后查看头结点的下一个结点,如果下一个结点不为空且它的waitStatus<=0,表示后继结点没有被取消,是一个可以唤醒的结点,于是唤醒后继结点返回;如果后继结点为空或者被取消了怎么办?寻找下一个可唤醒的结点,然后唤醒它返回。
这里并没有从头向尾寻找,而是相反的方向寻找,为什么呢?
因为在CLH队列中的结点随时有可能被中断,被中断的结点的waitStatus设置为CANCEL,而且它会被踢出CLH队列,如何个踢出法,就是它的前趋结点的next并不会指向它,而是指向下一个非CANCEL的结点,而它自己的next指针指向它自己。一旦这种情况发生,如何从头向尾方向寻找继任结点会出现问题,因为一个CANCEL结点的next为自己,那么就找不到正确的继任接点。
tryRelease方法主要是做了一个释放锁的过程,将同步状态state -1,直到减到0为止,这主要是兼容重入锁设计的,同时setExclusiveOwnerThread(null)清除当前占用的线程。这些head节点后的线程和新进的线程就可以开始争抢。这里需要注意的是对于同步队列中的线程来说在setState(c),且c为0的时候,同步队列中的线程是没有竞争锁的,因为线程被park了还没有唤醒。但是此时对于新进入的线程是有机会获取到锁的。
因为在setState(c)释放了锁之后,是没有线程竞争的,所以head是当前的head节点,先检查当前的Node是否合法,如果合法则unpark it。(阻塞的线程只有被唤醒之后才会继续执行相关代码)开始锁的获取。就回到了上面的for循环执行获取锁逻辑:
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 void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
setHead方法里面的前驱Node是Null,也没有线程,那么为什么不用一个在等待的线程作为Head Node呢?
因为一个线程随时有可能因为中断而取消,而取消的话,Node自然就要被GC了,那GC前必然要把头Node的后继Node变为一个新的头而且要应对多种情况,这样就很麻烦。用一个没有thread的Node作为头,相当于起了一个引导作用,因为head没有线程,自然也不会被取消。
再看一下上面unparkSuccessor的14行~20行,就是为了防止head的下一个node被取消的情况,这样,就从尾到头遍历,找出离head最近的一个node,对这个node进行unPark操作。
总结下来,如果线程1要释放锁,会先将锁的state减到0为止,并在等待队列里面找到一个最前面的未被取消的已被阻塞的节点,将其唤醒并获取锁。这个线程就是线程2.
ReentrantLock的公平锁实现与非公平锁实现比较相似,最大的差别就是新的线程获取锁时不能插队,必须老老实实加入到等待队列中。不再重复叙述。
这篇博文综合了几篇网上的博文,再加上一定自己的理解写的。有重复的部分侵删。