四、通过ReentrantLock理解AQS原理
2. ReentrantLock加锁机制:
在上篇文章学习到了
- 创建ReentrantLock对象
- 初次加锁的逻辑
- 同一线程第二次加锁 (可重入性)的逻辑。
现在我们回到我们最初的代码。
public class ReentrantLockDemo1 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁成功,开始执行!");
}, "Thread A");
Thread threadB = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
lock.lock();
System.out.println(Thread.currentThread().getName() + "获取锁成功,开始执行!");
}, "Thread B");
threadA.start();
TimeUnit.SECONDS.sleep(2);
threadB.start();
}
}
当线程A第一次进入获取到了锁。这个时候线程B在A未释放 资源之前是无法获取到锁的。那么AQS又是怎么做的?
3. 不同线程锁的竞争
此时,我们关注的重点是在线程B调用ReentrantLock的lock方法,底层到底是怎么做的。
此时state是1,所以同样会走NonfairSync非公平锁的tryAcquire(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;
}
此时,既不是锁持有线程,state又不是0。所以直接返回false,而返回false后。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
因为判断第一段为true,所以首先会进入addWaiter(Node.EXCLUSIVE), arg)方法。
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,并将当前线程放入,mode为EXCLUSIVE的模式,因为传入的是EXCLUSIVE,所以为独占锁。即只允许一个线程访问。看看接下来的逻辑:
- 将尾部节点tail赋值给上一个节点pred(此时尾节点为空,所以pred节点也为空)
- (因为pred节点为空,跳过非空判断体)执行enq(node)。传入以当前想要获取锁的线程为基础的Node对象。
我们来看看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;
}
}
}
}
终于第一次在相关代码中看到自旋的概念。此时enq就是执行一个自旋的过程。
- 第一次进入因为tail是空的,所以Node t也是空。
- 如果是空的,则执行compareAndSetHead原子操作将一个新的空Node存到双向队列的头部。并且将tail=head,尾部即位头部。
- 第二次自旋进入。t不为空,他已经是一个空的Node节点。将传入的node(带有想要获得锁线程的Node)的前一个位置prev填入这个空节点。执行原子操作当尾部等于t的时候,改尾部为传入的node。然后将t的下一位next改为传入的node。返回t这个Node,跳出自旋。
可见,只有当t节点的尾部被改为想要获得锁线程的Node的时候,自旋才会退出。
最终enq方法执行完成,返回了一个当前线程为想要获取锁的线程ThreadB,并且拥有一个空的prev节点的Node。最终addWaiter方法获取到一个node并作为参数传入到acquireQueued方法中。该方法是acquire方法第判断体第二段。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们来看这个方法:
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);
}
}
遇见的第二个自旋就在该方法中。我们传入了一个拥有当前想要获取锁ThreadB的Node。并且1作为第二个参数。看看他的逻辑step:
- 获取当前节点的上一个节点(上一个节点为空的头部节点)
- 再次调用tryAcquire(1)去尝试获取锁,(因为线程A未释放,不可能获取到)
- 将上一个节点和当前节点传入到shouldParkAfterFailedAcquire做第一个判断。
当前主要判断上一个节点状态waitStatus是否为-1(SIGNAL) 该状态为需要unpark释放。
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为0,执行compareAndSetWaitStatus设置上个节点的waitStatus为-1。(上个节点本来为空节点,所以需要标记为unpark)。返回false
- 继续回到acquireQueued中自旋。又到shouldParkAfterFailedAcquire做判断,发现上个节点的waitStatus已经是-1.直接返回true。
- 开始执行acquireQueued判断体第二部分parkAndCheckInterrupt,非常简单粗暴。使用LockSupport的park,将当前非公平锁传入作为blocker对象。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
至此,结束线程B的求锁之路。
如果线程A此时释放了锁,会调用unpark方法唤醒该线程。线程又返回到acquireQueued中自旋,尝试调用tryAcquire(1)方法去获得锁。如果获得成功,则将该节点设置为head节点。双向队列中等待的线程依次继续尝试获取锁,只是当前线程获得做执行了,所以队列中需要把他设置为head节点供后续节点等待。