其实一直想专门写aqs(AbstractQueuedSynchronizer),但是发现这个类功能有点广泛,设计理念更是比较庞大。
可能以我的能力应该是先写jdk中应用到这个aqs的类,然后再重新回过头来整理aqs才是比较合理的思路。
而其中最常用而且直接的类应该就是ReentrantLock(重入锁)了。
要看懂这个需要基本了解aqs的一些概念:同步队列以及节点状态位。
这里我们只分析两个核心方法NonfairSync的lock和unlock,其中lock比unlock会简单很多。
先看看NonfairSync的类层次
可以看到非公平锁NonfairSync是间接继承了aqs
可以看到ReentrantLock单纯地实现了Lock接口,里面又有Sync,NonfairSync,FairSync作为内部类,可以理解为一个包装类了。
Lock
通过看ReentrantLock的入口方法的注释来预热一下:
/**
* Acquires the lock.
*
* <p>Acquires the lock if it is not held by another thread and returns
* immediately, setting the lock hold count to one.
*
* <p>If the current thread already holds the lock then the hold
* count is incremented by one and the method returns immediately.
*
* <p>If the lock is held by another thread then the
* current thread becomes disabled for thread scheduling
* purposes and lies dormant until the lock has been acquired,
* at which time the lock hold count is set to one.
*/
public void lock() {
sync.lock();
}
- 如果当前锁没有被其他线程持有则马上可以获取锁并立刻返回,然后将计数设置为1;
- 如果当前的线程已经持有了锁,那计数+1,且方法立刻返回;
- 如果当前的锁被其他线程持有,然后这个线程则会被线程调度所禁用,并且维持休眠直到能够获取锁,当这个线程能够获取锁的时候,计数器设为1。(这个情况该方法会阻塞)
直接点进去看NonfairSync 都lock方法。
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
和注释描述的一样,这个方法在if的地方直接尝试暴力CAS来获取锁状态,成功的话当前线程置为owner然后就结束了。
然后看失败后的else,
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
注意这个方法是aqs的方法了,也就是说这个应该是一个通用方法(因为锁可以通过继承aqs来实现)!
先看tryAcquire(arg),注意这个方法是一个模板方法,是交由子类实现的
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
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找到了实现。
这里面的思想就是非公平获取的思想:如果当前无锁则直接去暴力CAS抢锁(不管那些在aqs同步队列里面等了大半天的线程节点),或者看看当前线程是不是持有了锁,那这里就可以单线程操作将计数器加一就好了(因此叫做重入锁)。
如果是抢到了锁,或者当前线程已经持有了锁了,那就结束了完事了。
如果还是没拿到锁,至此当前的线程已经两次cas抢锁失败了,是时候要用极端办法了。
回看acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire返回false的话下一步就是看addWaiter(Node.EXCLUSIVE)方法了。
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new 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;
}
先以该线程创建一个独占锁节点作为线程节点,然后队列非空的话尝试cas加入等待队列的队尾;
先不看return 因为不管这里这么样return都是返回当前线程节点的。
先看cas加入队尾失败的情况(cas失败或者队列为空)进入enq方法
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
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;
}
}
}
}
这里的逻辑比较简单,就是先看队列是不是空,空的话建立一个空的node作为head和tail。然后就是不停地自旋cas尝试让当前线程节点加入同步队列的尾端。
意思已经很明确了,这个线程必须一定要进同步队列!注意这里是return t,node的前驱节点,但是调用它的addWaiter方法并没有取他的返回值!我之前就是看这里被enq返回值误导了很久。
继续看acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
进去了acquireQueued方法,获取队列,
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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,而且这个方法是不可中断的方法。
这个方法就是再给刚刚入队的方法一个交代,看看他到底应该怎么往下走。
这里同样是一个自旋操作,但是一般情况下这个方法不会像前一个入队方法一样无节制地自旋,无论如何都要入队那种。
如果他是头结点,那么他就要去再试试能不能抢到锁,不行的话就要看是不是应该park休息一下。
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
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;
}
这个方法我一开始看真的很抽象。这里我们要先思考这个方法应该怎么实现,这个方法传参是当前线程节点及其前驱,方法名叫做如果取锁失败是否应该休息,那什么时候应该休息呢?你前面如果有很多排队取锁的线程,而且他们个个很生猛的时候,是不是就不要去凑热闹了?
这个方法正是这个意思,先看前一个节点是不是signal状态,是的话就返回true,这时候就不自旋了,可以休息了。
如果前驱的状态>0那就是cancelled了的,坑爹货,那要一直循环看他前面还有没有坑货,直到找到一个不是很坑的货(或者是head节点),重塑节点连接关系。
如果前驱的状态<0且不是singal,那就让他成为signal(这里涉及到aqs知识,请看aqs状态解释),返回false,出去继续自旋。
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
上述方法返回true会进入该方法,意思是休眠而且返回当前节点的中断状态以及清空中断状态。
现在有两个小问题:
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);
}
}
这个方法中为什么这么关注中断状态?
什么时候会能进入到cancelAcquire呢?
我说说我的理解
对于第一个问题:
acquireQueued 方法体明确说了是uninterruptible的,但是线程如果在执行过程中被其他线程提示中断了怎么办呢?那总不能丢失掉中断状态吧,那只能将中断状态保存起来,返回给上层,如果被中断了,然后再在上面的acquire方法调用selfInterrupt,将中断位保存住。核心就是要保证被中断的话中断信息不丢失。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
第二个问题:
如果但看非公平锁的实现是只有在tryAcquire方法中抛异常才能进入这个cancelAcquire,但是这个异常又是锁重入次数超限才能发生的,是只有这种情况了嘛,那也太少几率发生了吧。事实上应该是由于tryAcquire是一个模板方法,是可以给予其他框架和个人来实现的一个方法,方法体里面可以允许自由地抛异常,那么在这种时候,就可以进入cancelAcquire来做一些清理工作了。
这个cancelAcquire方法比较难,看了好几遍都没看懂,以后有机会再来补充了。
这里我们总结一下一个线程调用lock的流程:
- 先会有两次尝试cas取锁的机会
- 都失败的话有一次cas入aqs同步队列尾的机会
- 再失败的话自旋进入同步队列尾端
- 成功入队列之后看情况下一步怎么走:万一能成为队列头 则继续cas尝试获取锁,否则找机会休息一波再战。
unlock
/**
* Attempts to release this lock.
*
* <p>If the current thread is the holder of this lock then the hold
* count is decremented. If the hold count is now zero then the lock
* is released. If the current thread is not the holder of this
* lock then {@link IllegalMonitorStateException} is thrown.
*
* @throws IllegalMonitorStateException if the current thread does not
* hold this lock
*/
public void unlock() {
sync.release(1);
}
注意的是,如果该线程没持有该锁,则会抛异常。
实现是在aqs里面的:
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease方法是在ReentrantLock里面的
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;
}
回看release,如果是这个线程进入了这个锁不止一次,那就是会返回false;
如果不是的话,那就是进入unparkSuccessor方法来对头部方法解锁;
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
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);
}
这个方法的核心思想就是找一个节点唤醒。
一开始会尝试头结点指向如果是坑爹货,那就放弃他了,直接从尾端开始找,找到第一个就唤醒它。
唤醒之后做什么呢?
那就是从acquireQueued方法中醒来,继续自旋看看自己是不是头节点了从而找机会抢锁出队列了。
这里有个疑问没解决:
为什么这个unparkSuccessor方法这么大胆直接将头的下一个节点置空?然后从尾端开始往前找第一个waitstatus成立的节点唤醒呢?为什么不是找最前一个呢?
个人的猜测是在ReentrantLock中,不会发现head指向的下一个节点的是null或者cancelled的情况,for循环是不会进入的。
要解答这个问题,需要明白的是头节点的连接什么时候能够得到重置,还有就是线程节点的waitStatus的值的变化。
解锁的方法比较简单,也没什么可以解决的,如果谁能解答最后这个疑问,麻烦能留言告诉我一下,谢谢!