一马平川
不积跬步无以至千里,后继才能薄发

AbstractQueuedSynchronizer(AQS)原理笔记

2022年07月02日
0
未分类

队列同步器的作用

在多线程应用中,关键在于对状态依赖的管理以及对线程状态的管理
因为资源不是无限的,如I/O资源、内存资源、CPU资源等,需要对线程进行控制,获取不到资源的线程需要进行阻塞或者挂起,这就引入了队列同步器中的一个关键属性:队列,获取不到资源的线程需要阻塞,然后在队列中等待唤醒。

状态依赖管理

对于加锁有前验的条件验证,也就是状态依赖。如:一个阻塞队列,put的时候队列不能为满,get的时候队列不能为空等。在一个线程安全类中,通常不止一个需要唤醒线程情况,如果使用Synchronized进行实现,就会产生不少问题,在下面会进行讲解。

条件队列

如果我们能够将等待不同条件的线程分开进行管理,如一个阻塞队列,在get为空的时候阻塞,则需要被put的操作唤醒,而不是get操作,这时候就引入了条件队列这一概念了,条件队列可以使得一组线程组成一条或多条队列,对特定条件进行排队等待。

条件谓语

条件谓语就是线程能够继续执行的前验条件,一个条件队列可能会有一个或多个条件谓语,条件谓语成真时线程继续执行,否则等待谓语成真。一个条件队列就是一组线程等待一个条件谓语组成的,多个条件队列就组成了一个完整的同步工具类,如 ReentrantLock

线程唤醒时的意外情况

过早唤醒

由于一个条件队列中可能会有多个条件谓语,在线程被唤醒时,可能并不意味着这个线程等待的条件谓语成真,所以需要在被唤醒之后再进行一次条件谓语的验证,不为真则继续等待。

丢失信号

线程A通知了一个条件队列,线程B随后在这个条件队列中等待,线程B错过了通知需要等待另一个通知才能醒来。在线程等待之前没有做条件谓语为真的校验就会导致丢失信号。

AbstractQueuedSynchronizer的组成

  • 资源状态 State
    在独占锁的场景下, 可以理解为资源占有的标识位; 在共享锁的情况下, 可以作为资源数量理解.

  • 双向等待队列(CLH队列)
    CLH队列全称是(Craig, Landin, andHagersten) lock queue, 用来存储被阻塞的线程信息.

独占锁和共享锁

  • 共享锁
    可以有多个线程同时执行, 线程执行个数也会受到state限制, 如Semaphore,CountDownLatch等等

  • 独占锁
    当前只有一个线程能够运行, 如ReentrantLock;

公平锁和非公平锁

  • 公平锁
    在请求资源时,如果无法获取,严格按照队列顺序,进入等待队列。
1
2
3
4
//ReentrantLock.java
final void lock() {
acquire(1);
}
  • 非公平锁
    每个线程不管是否有其他线程等待, 都会先通过CAS操作尝试抢占锁, 抢占失败再去CLH队列中等待锁资源释放
1
2
3
4
5
6
7
//ReentrantLock.java
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

CAS操作

AQS在获得资源状态state, 以及添加阻塞线程到CLH队尾时都是使用CAS操作解决线程间资源抢占问题的

  • 获取资源
1
compareAndSetState(0, 1);
  • 添加线程到CLH队尾, Node中封装线程及锁独占或共享标识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//AbstractQueuedSynchronizer.java
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;
}

线程阻塞及唤醒

在AQS中, 当线程抢占资源失败时, 会调用unsafe.park()方法阻塞线程;

当其他线程释放资源时, 也会调用unsafe.unpark()唤醒线程;

条件谓语

AQS中将条件谓语封装成 Condition ,一个 Condition 会记录线程等待头节点和尾节点,实现在唤醒时对需要的阻塞线程进行唤醒

1
2
3
4
5
6
7
8
9
//AbstractQueuedSynchronizer.java
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
//省略其他代码
}

如果喜欢这篇文章,可以给作者评个份哦~

原文声明: "转载本站文章请注明作者和出处Nothinglin ,请勿用于任何商业用途"

公众号:苦逼的学生仔