引言
本文参考了CSDN博主为zejian_的深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理一文。那篇文章对ReentrantLock的讲解得非常好,看完我也收获很多。不过当自己尝试写些东西的时候,发现原来自己并没有收获多少。别人写出来的东西始终是别人的,你看过了也不代表你都吸收了。因此,在写这篇文章的时候,遇到了许多问题,也参考了其他文章。原本一些以为懂的知识点也在写文章的过程中真正的理解了。
因此,虽然这篇文章,涉及的内容远不如上面链接的那篇文章多,思路也不如他那么清晰,但是于我自己而言,却是很有意义的。
这篇文章,只是通过分析ReentrantLock的lock方法和unlock方法来了解AQS并发框架,所有没有涉及到等待队列的相关信息。在后续的时间里,会继续学习,再补上这块内容。
下面以Lock作为入口,进行分析。
ReentrantLock
Lock接口有6个方法:
public interface Lock {
//获取锁,获取不成功则一直等待,直到获取成功
void lock();
//获取锁,可中断
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,获取失败则返回false,成功则返回true
boolean tryLock();
//尝试获取锁,超过指定时间都还没获取成功,则返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//获取与该锁相关的等待队列
Condition newCondition();
}
而ReentrantLock是Lock接口的实现类之一,其常见用法如:
Lock lock = new ReentrantLock();
lock.lock();
try{
//临界区
}finally{
lock.unlock();
}
我们知道,当前线程使用lock()方法和unlock方法()对临界区进行包围,使得其他线程由于没有获取锁而无法进入临界区,直到当前线程释放了锁。
但是,你知道lock()方法和unlock()是如何实现这个效果的吗。我也问过自己这个问题,于是,我才开始去了解相关的源码以及查看相关的源码解读文章。
下面,以Lock接口的实现类ReentrantLock来讲述lock()方法和unlock()方法是如何使得在一个时间段中有且只有一个线程能对共享变量进行操作,即如何实现锁的作用。
实际上ReentrantLock是基于AQS并发框架实现的。ReentrantLock中有一个非常重要的属性——sync:
private final Sync sync;
ReentrantLock中的绝大多数方法的实现都是调用了sync属性的方法,举例如:
public void lock() {
sync.lock();
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isLocked();
}
Sync即Synchronizer的缩写,表示同步器,该类继承自AbstractQueuedSynchronizer(AQS),也是一个抽象类:
abstract static class Sync extends AbstractQueuedSynchronizer
AbstractQuenedSynchronizer队列同步器
AbstractQuenedSynchronizer(AQS),即队列同步器,它是构建锁的基础框架。它的内部有一个state属性,当state的值为0时,表示当前没有任何线程占有共享资源的锁。当state的值为1时,表示当前有线程占有共享资源的锁。当共享资源的锁被某个线程占有时,倘若这时另一个线程通过调用lock方法尝试获取锁,该线程会被封装成一个Node节点加入到同步队列中排队等候获取锁。
如图所示,head指针指向队列的头,tail指针指向队列的尾。且同步队列采用的是双向链表的结构,除了头节点,每个节点封装着等待获取锁的线程。注意,头节点不存储任何信息,头结点是在什么时候产生的,我们后面再来分析。
正是通过这个同步队列,实现了共享资源的同步访问。
如何使得线程处于等待获取锁状态呢?通过循环,当且仅当当前线程所属的节点是同步队列中第一个节点且state为0时,该线程才可能获取到锁,否则会进入睡眠等待状态。(为什么说才可能获取到锁呢,因为这是考虑的是非公平锁。且这里没有考虑等待队列)
ReentrantLock是可重入锁,它又可以再进行细分:非公平的可重入锁以及公平的可重入锁。公平锁指的是先调用lock方法的线程会先获取锁,后调用lock方法的线程后获取锁,不会出现后调用lock方法的线程先获取锁的情况。非公平锁则相反,后调用lock方法的线程有可能先获取锁,注意这里说的是有可能。具体是什么情况呢。假设同步队列中有三个正在等待获取锁的线程ABC,当获取了锁的线程Z释放了锁,state变为0,此时线程A刚好在判断自己所在的线程是否满足“当前线程所属的节点是同步队列中第一个节点且state为0”,恰好线程D执行了lock方法,发现state为0,于是就获取到锁了。因此,就产生了后调用lock方法的线程先获取锁的情况。
默认的ReentrantLock实现的是非公平锁。但我们可以通过有参的构造方法选择实现公平锁的ReentrantLock。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
从构造方法我们也可以看出,NonfairSync和FairSync都是Sync的具体实现。
下面通过分析基于非公平锁的ReentrantLock的lock方法和unlock方法来了解它的内部实现。
但在这之前先来看看几个主要的类与接口的关系图:
(该图来自CSDN博主为zejian_的一篇文章深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理)
ReentrantLock的lock方法
ReentrantLock中的lock方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
该方法中,通过一个CAS操作,尝试将队列同步器的state从0改成1,如果操作成功,说明当前共享资源的锁不被任何线程占有,因此可以占有该锁,于是执行上锁操作。如果操作失败,说明当前共享资源的锁以被其他线程占用,此时执行acquire(1)方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法的实现,是在AbstractQueuedSynchorinizer类中,在该方法中,调用tryAcquire(arg)方法再次尝试去获取锁,如果获取成功,则调用结束。如果获取不成功,则先执行addWaiter(Node.EXCLUSIVE), arg)方法,将当前线程封装成一个节点,再调用acquireQueued方法将封装好的节点加入到同步队列中。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
tryAcquire方法的实现,是在NonfairSync类中,即Sync类的子类中,这样做是因为公平锁与非公平锁获取锁尝试获取锁的方式不一样,因此父类Sync提供抽象方法tryAcquire,而两个子类为该方法提供具体的实现。
注意,在NonfairSync类的tryAcquire()方法中,调用了nonfairTryAcquire(acquires)方法,但nonfairTryAcquire方法却是其父类Sync中的方法,为什么不是定义在NonfairSync类中呢?这是因为该方法不仅用于子类NonfairSync的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;
}
在nonfairTryAcquire方法中,通过getState方法获取state的值,如果该值为0,说明当前共享资源的锁不被任何线程占用,则使用CAS方法尝试修改state的值,若成功,则可以上锁。若不为0,则判断当前线程是否占用锁的线程,如果是,则再上一次锁(可重用锁,即可以多次加锁),将state的值加1.如果当前线程不是占用锁的线程,则表示当前线程的上锁操作失败。
前面说了,当tryAcquire方法调用返回false,即获取锁失败了,则会将线程封装成节点加入到同步队列中。
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;
}
addWaiter(Node.EXCLUSIVE)方法,通过Node的构造方法将当前线程封装到节点node中,然后判断同步队列的头结点是否为空,不为空,则通过CAS操作尝试将节点node接到同步队列的尾部,若成功,则返回该节点。若失败,表示不止当前这个线程在执行这个addWaiter操作,因此执行enq(node)方法,通过循环以保证将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;
}
}
}
}
enq(final Node node)方法很重要。前面我们说了,同步队列的头节点不存储任何线程信息。而head指针,正是指向这个头节点。那么是怎么实现的呢。调用enq方法,当tail指针指向null时,说明同步队列中没有任何节点,说明这是第一个调用enq方法,则会新增一个空的Node节点并通过CAS操作将其设置为同步队列的头结点。也就是说,头结点是在第一次调用enq方法时产生的。以后再调用enq方法,执行的将是else中的操作,会将封装好的节点接到同步队列的尾部。
addWaiter方法执行完后,返回新增的节点,于是调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法。
我们再来看看acquire方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
现在只剩下acquireQueued方法还没调用了。前面所进行的操作就是获取锁失败后将该线程封装成Node节点并加入到同步队列中。但是这些加入到同步队列的线程不能往下执行临界区中的操作,我们知道这些线程会去等待获取锁,那么究竟是怎么个等待法?这就是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);
}
}
interrupted属性是用于lockInterruptibly()方法的,这里分析的是lock()方法,因此用不上,不管它。
在acquireQueued方法中,先判断当前节点的上一个节点是否头结点,如果不是,则尝试让当前线程进行睡眠状态。如果当前节点的上一个节点是头结点,则尝试获取锁,如果获取失败,同样,尝试让当前线程进入睡眠状态。让线程进入睡眠状态,是以防止无止休的循环。如果获取成功,则该线程
parking是睡觉的意思,unparking是唤醒的意思。
在Node节点中,有这么一个属性:waitStatus。
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
如注释中所解释,
当waitStatus的值为 1时,表示该线程节点中的线程已被取消。
当waitStatus的值为-1时,表示下一个的线程需要被唤醒,也就是说下一个节点中的线程处于睡眠状态。
当waitStatus的值为-2时,表示该线程节点中的线程处于等待状态。(此时该线程节点是处于等待队列中而不是同步队列中)
shouldParkAfterFailedAcquire方法,从方法名字就可以猜到它的内容:在获取锁失败之后是否应当睡眠。
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;
}
这里,对shouldParkAfterFailedAcquire方法进行分析。
如果上一个节点的waitStatus属性值为-1,表示后续的线程需要被唤醒,则设置OK了,直接返回true。往下执行parkAndCheckInterrupt()方法,让线程安心睡觉,等着被唤醒。
如果上一个节点的waitStatus属性值大于0,对应状态为CANCAELLED,则通过do while循环找到上一个waitStatus不大于0的节点,并设置好这两个节点的指针。该片段的作用就是移除前面waitStatus为NODE.CANCAELLED的节点。最后返回false。
如果上一个节点的waitStatus属性值小于等于0且不为-1,正如注释所写,waitStatus must be 0 or PROPAGATE。而在ReentrantLock中,则只剩下值为0的情况(当节点waitStatus的值为-2时,该节点是处于等待队列中而不会存在同步队列中)。则通过CAS操作将上一个节点的waitStatus属性设置为-1。由于CAS操作不一定成功,即不能保证上一个节点的waitStatus属性会被改为-1,因此不能安心睡觉,于是返回false,在下一轮循环的时候再来检查是否修改成功。这里由于返回false,因此不会执行parkAndCheckInterrupt方法。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
若shouldParkAfterFailedAcquire方法返回true,则会执行parkAndCheckInterrupt方法,在该方法中,会通过LockSupport.park(this);方法让当前线程进入睡眠状态。以避免无止休的for循环。至于Thread.interrupted()方法,则是对应调用lockInterruptibly()方法的情况。判断当前线程是否被中断,是则返回true,不是则返回false。对于lock方法而言,parkAndCheckInterrupt方法会返回false。
上面的分析中,忽略了等待队列的情况。以后再补充,至此,lock方法就分析完了。
下面对unlock方法进行分析。
ReentrantLock中的unlock方法
ReentrantLock中的lock方法
public void unlock() {
sync.release(1);
}
该方法,继承自AbstractQueuedSynchronizer类。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在release方法,会尝试释放共享资源,若失败,则返回false,操作结束。先来看看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方法,是AbstractQueuedSynchronizer中定义的抽象方法,在Sync类中实现了。该方法的作用是释放共享资源。什么意思呢。tryRelease方法会释放一把锁,但是释放一把锁并不表示共享资源就没被锁住了。因为有可能该线程对共享资源上了不止一把锁。
在该方法中,形参releases为1,参数c表示的是当前state值减一后的结果值。如果c的值为0,则设置exclusiveOwnerThread属性为null表示当前共享资源不被任何任何线程占有锁。再返回true,表示共享资源没有没锁住。如过c的值不为0(c的值不可能小于0),则表示c的值大于0,则表示当前线程不止被上了一个锁,则将c的值赋给state,表示减去一把锁。并返回false表示共享资源还是被锁着。
如果tryRelease方法返回true,则表示共享资源没被锁住了。就尝试着去唤醒下一个节点,因此执行release方法中的这部分代码:
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
当if成立的时候会去调用unparkSuccessor方法。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
如果头结点的waitSatus小于0,即对应waitStatus为NODE.SIGNAL的情况,则将其设置为0,
通过for循环从队列尾往前找,找到处于等待获取锁状态且最靠近头结点的那个节点,让s指向的该节点,通过LockSupport.unpark(s.thread)唤醒该节点中的线程。
如果s指向的为null,表示没有需要被可以被唤醒的节点,则直接结束。
收获
当看完别人写的文章的时候,感觉懂了,自己再分析的时候,却出现了各种各样的问题,果然,实践是很重要的。另外,在分析源码的过程中,阅读源码中的相关注释非常有必要。在写这篇文章的时候,就因为没看源码相关注释,对Node类的waitStatus属性又没理解对,以致于阅读源码时对流程的处理无法理解。
参考文章
http://blog.csdn.net/u013159433/article/details/51407320
http://blog.csdn.net/javazejian/article/details/75043422?locationNum=1&fps=1