本文是自己的学习笔记,主要参考资料如下
JavaSE文档
- 1、AQS 概述
- 1.1、锁的原理
- 1.2、任务队列
- 1.2.1、结点的状态变化
- 1.3、加锁和解锁的简单流程
- 2、ReentrantLock
- 2.1、加锁源码分析
- 2.1.1、tryAcquire()的具体实现
- 2.1.2、acquirQueued()的具体实现
- 2.1.3、tryLock的具体实现
- 2.1.5、总结
1、AQS 概述
1.1、锁的原理
AQS是指抽象类AbstractQueuedSynchronizer
。这个抽象类代表着一种实现并发的方式。
具体实现方式是使用volitile
修饰state
变量,保证了state
的可见性和有序性。最后使用CAS
改变state
的值,保证原子性。
那么AbstractQueuedSynchronizer
通过更新state
的值来实现的加锁和解锁。
下面是关键源代码的截图。
1.2、任务队列
AQS
中维护了一个任务队列,是一个双向队列。队列节点是内部类Node
。
在Node
中记录者节点的状态waitStatus
,比如CANCEL
,SIGNAL
等分别表示该任务节点已经取消和任务节点正在沉睡需要被唤醒。
当然,因为是双向列表所以也有指向前后节点的指针。下面是Node
源码的部分截图。
这个队列会初始化一个头结点和一个尾结点作为虚拟节点。头结点的状态在整个加锁和释放锁的过程中都会变化。
1.2.1、结点的状态变化
当头结点指向的Node
才拥有锁。
这里主要介绍三个状态
0
, 表示当前Node
后续无节点在排队。不表明是否拥有锁。-1
,表示除了当前Node
在排队以外,还有其他Node
排在当前Node
后面。不表明是否拥有锁。1
,表示当前Node
可能因为等待时间太长而放弃获取锁。
下面是三个Node
在队列中的状态。这里从左到右解释他们的状态。
head
指向第一个Node
,所以当前Node
拥有锁。
第一个Node
的waitStatus=-1
表示后续有节点等待获取锁。当该节点释放锁时会唤醒后续的节点。
第二个Node
的waitStatus = -1
,后续有节点等待获取锁。
第三个Node
的waitStatus = 0
,后续无节点等待获取锁。
1.3、加锁和解锁的简单流程
假设有两个线程A和B,他们需要争夺基于AQS
实现的锁,下面是争夺的简单流程。
- 线程A先执行CAS,将state从0修改为1,线程A就获取到了锁资源,去执行业务代码即可。
- 线程B再执行CAS,发现state已经是1了,无法获取到锁资源。
- 线程B需要去排队,将自己封装为Node对象。
- 需要将当前B线程的Node放到双向队列保存,排队。
2、ReentrantLock
2.1、加锁源码分析
ReentrantLock
分为公平锁和非公平锁。在加锁的时候因这两种锁的不同会有不同的加锁方式。
ReentrantLock
默认是非公平锁,构造方法中传入false
则是公平锁。
非公平锁的lock()
方法会直接基于CAS
尝试获取锁,如果成功的话则执行setExclusiveOwnerThread()
方法表示当前线程持有该锁;如果失败则执行acquire()
方法。
公平锁则是直接执行acquire()
方法。下面是源码对比。
接下来的重点则是看acquire()
的具体操作。
tryAcquire()
方法会再次尝试获取锁,如果成功返回true
,否则返回false
。
可以看到如果失败的话则将请求放到等待队列中同时发送中断信号。
2.1.1、tryAcquire()的具体实现
- 非公平锁
非公平锁会尝试再次直接通过CAS
获取锁资源。因为是可重入锁,所以当锁的持有者是当前线程时也可直接获取锁,然后计数器加一。
- 公平锁
公平锁的逻辑与非公平锁类似,只不过再获取锁之前会先判断AQS
中自己是不是排在第一位,之后才会获取锁。
2.1.2、acquirQueued()的具体实现
当tryAcquire()
返回false
,即获取锁失败,就开始尝试将当前线程封装成Node节点插入到AQS
的结尾。
在插入时我们会看到if(p == head && tryAcquire(arg))
这样的语句。
这是因为AQS
有伪头结点,所以当这个线程插入到AQS
中时发现自己的上一个节点是头结点,即自己排在第一位,那无论是公平锁还是非公平锁自己都可以再次测试获取锁。所以会再次执行tryAcquire()
。
final boolean acquireQueued(final Node node, int arg) {
// 不考虑中断
// failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到当前节点的前继节点
final Node p = node.predecessor();
// 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。
if (p == head && tryAcquire(arg)) {
// 获取锁资源成功
setHead(node);
p.next = null;
// 获取锁失败标识为false
failed = false;
return interrupted;
}
// 没拿到锁资源……
// shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,
// 如果不能,就返回false,继续下次循环
if (shouldParkAfterFailedAcquire(p, node) &&
// 这里基于Unsafe类的park方法,将当前线程挂起
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 在lock方法中,基本不会执行。
cancelAcquire(node);
}
}
2.1.3、tryLock的具体实现
无参的tryLock()
比较简单,和tryAcquire()
基本没区别。
这里主要讲解有参的tryAcquireNanos(int arg, long nanosTimeout)
。
它的作用在一个时间内尝试获得锁。在这个时间内没有获得锁会挂起park
线程。如果成功则返回true
,时间结束还没有获得则返回false
。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
该方法需要处理中断异常,和lock()
方法不一样。
我们继续深入。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
可以看到,它直接通过线程的中断标志位决定是否抛出异常。
之后进行tryAcquire()
,这个方法细节上面分析过,它有公平和非公平两种实现,简而言之就是非公平直接尝试CAS
加锁,公平则是进入队列排队。
也就是说,最后它会正常加锁,只有失败时才会执行doAcquireNanos()
。所以有参的tryLock()
方法park
线程的细节就在其中。
那下面就看看这个方法的内部。
核心就是线程会被封装Node
放到队列中,之后查看时间,如果时间比较长,就park
线程直到时间结束后再尝试获取锁;如果时间比较短,就在死循环中等到时间结束然后再次获得锁。
因为park
的线程主要会因两个动作结束park
,即时间到,或者线程发出中断状态,所以最后会查看park
是因为什么结束的。如果是中断则抛出异常,否则尝试获取锁。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果等待时间是0秒,直接告辞,拿锁失败
if (nanosTimeout <= 0L)
return false;
// 设置结束时间。
final long deadline = System.nanoTime() + nanosTimeout;
// 先扔到AQS队列
final Node node = addWaiter(Node.EXCLUSIVE);
// 拿锁失败,默认true
boolean failed = true;
try {
for (;;) {
// 如果在AQS中,当前node是head的next,直接抢锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 结算剩余的可用时间
nanosTimeout = deadline - System.nanoTime();
// 判断是否是否用尽的位置
if (nanosTimeout <= 0L)
return false;
// shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
// 避免剩余时间太少,如果剩余时间少就不用挂起线程
nanosTimeout > spinForTimeoutThreshold)
// 如果剩余时间足够,将线程挂起剩余时间
LockSupport.parkNanos(this, nanosTimeout);
// 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。
if (Thread.interrupted())
// 是中断唤醒的!
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.1.5、总结
ReentrantLock
的加锁有公平锁和非公平锁两种方式。
对于非公平锁,任务一开始会直接尝试通过CAS
获取锁,失败后才会进入任务队列。并且进入的时候会再次尝试获取锁。整个过程并不考虑其他节点等了多久,所以才是非公平锁。
对于公平锁,任务会按序先进入任务队列,直到有人唤醒他们才会开始获取锁。