ReentrantLock和AQS常见问题分析
一、前言
本文利用ReentrantLock作为阅读AQS的切入口,通过问答的方式让大家更好的去理解今天要掌握的点,也欢迎大家说说自己的答案。
二、本文大纲
脑图是个很好的辅助记忆工具,也能提高自己的逻辑思维能力,下文我会通过这个脑图来讲解。
三、问答环节
- 什么是AQS
AQS是抽象队列同步器,AQS内部维护了一个用volatile修饰的state变量和一个FIFO的双向队列,线程通过CAS修改state,如果CAS失败就将当前线程组装成Node添加到双向队列里面。很多同步工具都继承了AQS类。 - AQS能实现哪些同步工具
同步工具 | 同步工具与AQS的关联 |
---|---|
ReentrantLock | 使用AQS保存锁重复持有的次数。 |
Semaphore | 使用AQS同步状态来保存信号量的当前计数。 |
CountDownLatch | 使用AQS同步状态来表示计数。 |
ReentrantReadWriteLock | 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。 |
ThreadPoolExecutor | Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。 |
- 你能通过AQS实现一个同步锁吗?
可以,只需要继承AQS,在重写tryAcquire和tryRelease方法去操作State节点就能实现一个简易的同步锁了。
public class TestMyAQS extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
while (true){
if(compareAndSetState(0, arg)){
return true;
}
}
}
@Override
protected boolean tryRelease(int arg) {
setState(0);
return true;
}
}
- AQS中为什么要有一个虚拟的head节点
waitStatus维护了下个节点的状态,可能是挂起也可能是取消,而释放锁的时候需要挑选一个挂起的线程去释放锁,第一个挂起的线程没有前置节点,所以需要创建一个虚拟节点。 - AQS中为什么选择使用双向链表,而不是单向链表
在ReentrantLock中,当调用中断线程排队方法lockInterruptibly时候,会将waitStatus设置成1,从AQS队列中移除,如果是单项链表需要从头开始遍历,很耗时间。 - 如果头结点的下一个节点取消了,唤醒节点的时候为什么从后往前找
从代码可以看到插入的时候node的pred节点能保证一定不为空,如果从头向尾找可能出现调度问题,compareAndSetTail已经成功了,此时pred节点的next节点还是空的。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速加入到队尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
- ReentrantLock中公平锁和非公平锁有什么区别
- 公平锁多了一个方法保证拿到的锁的节点都是第一个节点
- 公平锁相对非公平锁性能要差
- 公平锁不会出现锁饥饿的问题
- 公平锁一定公平吗
不一定,假设A线程获取到了锁,B线程没获取到锁,正在添加到队列里的过程中,A线程释放锁了,C线程进来了,发现tail==head,于是后来的C获取到了锁。
步骤一、线程B添加队列
注释地方设置tail = head
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head; // B线程设置tail = head
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
步骤二、线程C尝试获取锁
hasQueuedPredecessors,由于h==t,返回false
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t && // 由于h==t,返回false
((s = h.next) == null || s.thread != Thread.currentThread());
}
步骤三、线程A释放锁
步骤四、线程C尝试获取锁
CAS成功,线程C获取到了锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) { // 这里CAS成功,线程C获取到了锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
- ReentrantLock和Synchronized的区别
- synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
- synchronized会自动释放锁,而Lock必须手动释放锁。
- synchronized是不可中断的,Lock可以中断也可以不中断。
- 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
- synchronized能锁住方法和代码块,而Lock只能锁住代码块。
- Lock可以使用读锁提高多线程读效率。
- synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。