AQS-Lock获取锁操作
获取锁
获取锁最直观的感受就是使用 Lock.lock () 方法来获得锁,最终目的是想让线程获得对资源的访问权。 Lock 一般是 AQS 的子类,lock 方法根据情况一般会选择调用 AQS 的 acquire 或 tryAcquire 方法。acquire 方法 AQS 已经实现了,tryAcquire 方法是等待子类去实现,acquire 方法制定了获取锁的框架,先尝试使用 tryAcquire 方法获取锁,获取不到时,再入同步队列中等待锁。tryAcquire 方法 AQS 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁,接下来我们详细看下 acquire 的源码解析。前面讲过,AQS 包含两种模式,独占模糊和共享模式,即通常讲的排它锁和共享锁。
获取排他锁
尝试获取锁
// 排它模式下,尝试获得锁
public final void acquire(int arg) {
// tryAcquire 方法是需要实现类去实现的,实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
if (!tryAcquire(arg) &&
// addWaiter 入参代表是排他模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其中 AQS 并没有 对 tryAcquire 方法做一个实际实现,就给了子类实现,其中包括:ReentrantLock、ReentrantReadWriteLock 锁都对 tryAcquire 做了自己的实现。
总体上获取排他锁可以分为下面四个步骤:
1、尝试执行一次 tryAcquire,如果成功直接返回,失败走 2;
2、线程尝试进入同步队列,首先调用 addWaiter 方法,把当前线程放到同步队列的队尾;
3、接着调用 acquireQueued 方法,两个作用,1:阻塞当前节点,2:节点被唤醒时,使其能够获得锁;
4、如果 2、3 失败了,打断线程。
加入等待队列
很显然,按照之前讲过的思路,获取不到锁资源的线程是需要被加入到队列中的,并且被阻塞起来。addWaiter 实现了将线程节点放到队列中。
private Node addWaiter(Node mode) {
// 新建一个线程节点
Node node = new Node(Thread.currentThread(), mode);
// 获取队尾线程节点
Node pred = tail;
// 如果队列不为空,之前已经有线程在队列中排队
if (pred != null) {
// 把新线程节点的的前指针指向队尾
node.prev = pred;
// 通过 CAS 操作把 tail 指向新的节点,保证 tail 永远指向最后一个线程节点(队列是在尾部添加线程节点)
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//自旋保证node加入到队尾
enq(node);
return node;
}
初始化队列
显然,以上是在队列不为空时的操作,时直接将新线程节点追加到队列末尾,这很符合队列的操作。那当队列为空时,队列的初始化是在 enq 中实现。
private Node enq(final Node node) {
for (;;) {
// 得到队尾节点
Node t = tail;
// 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
// tail = head = new Node();
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
// 队尾不为空,将当前节点追加到队尾
} else {
node.prev = t;
// node 追加到队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
从代码可以看出会先先初始化队列,主要是把 head 和 tail 指向新节点。关于 else 中的操作,我的理解是:这里除了初始化队列之外,还有对 addWaiter 的补偿,因为 compareAndSetTail(pred, node) 没有成功可以进入到 enq 直到成功为止并且返回。因为大部分情况下 compareAndSetTail(pred, node) 一次就会成功。
阻塞线程
在 acquire 函数中已经知道追加节点之后会调用 acquireQueued。而按照思路,线程节点加入队列之后因为是要被阻塞。直到其他获取到锁资源的线程释放资源。
// 返回 false 表示获得锁成功,返回 true 表示失败
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 选上一个节点
final Node p = node.predecessor();
// 如果 node 之前没有获得锁,进入 acquireQueued 方法时,发现自己是第一个线程节点,于是尝试获得一次锁;
// 我的理解是:这是一种幸运机制,有可能节点刚刚加入队列时,刚好锁资源被释放。那么这时候尝试去获取锁就刚好可以拿到
if (p == head && tryAcquire(arg)) {
// 获得锁,设置成 head 节点
setHead(node);
//p被回收
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断自己是否应该被阻塞,判断的标准在 shouldParkAfterFailedAcquire 方法中
// parkAndCheckInterrupt 阻塞当前线程,如果满足条件,节点对应的线程就被阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
// 线程是在这个方法里面阻塞的,醒来的时候仍然在无限 for 循环里面,就能再次自旋尝试获得锁
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获得node的锁失败,将 node 从队列中移除
if (failed)
cancelAcquire(node);
}
}
整个方法主要做两件事情:
1:通过不断的自旋尝试使自己前一个节点的状态变成 signal,然后阻塞自己;
2:获得锁的线程执行完成之后,释放锁时,会把阻塞的 node 唤醒,node 唤醒之后再次自旋,尝试获得锁。
总结一下,acquire 方法大致分为三步:
1、使用 tryAcquire 方法尝试获得锁,获得锁直接返回,获取不到锁的走 2;
2、把当前线程组装成节点(Node),追加到同步队列的尾部(addWaiter);
3、自旋,使同步队列中当前节点的前置节点状态为 signal 后,然后阻塞自己;
获取共享锁
共享锁,因为锁资源在同一个时刻可以被多个线程一起访问,和独占锁不同。找到获取共享锁的入口函数 acquireShared;
尝试获取锁
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
和独占锁相同,还是先尝试获取锁,失败了就去做资源入队操作,tryAcquireShared 并没有一个实际的实现,也是交给了子类做具体实现;在使用中可以知道 信号量(Semaphore)和 计数器(CountDownLatch)都有做了具体的实现。
加入等待队列
核心代码在 doAcquireShared 里面,这是获取共享锁的核心代码。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}