如果你现在打开 IDE, 你会发现上文提到的 ReentrantLock ReentrantReadWriteLock Semaphore(信号量) CountDownLatch 都是按照这个结构实现,所以我们就来看一看 AQS 的模版方法到底是怎么实现锁
AQS实现分析从上面的代码中,你应该理解了lock.tryLock() 非阻塞式获取锁就是调用自定义同步器重写的 tryAcquire() 方法,通过 CAS 设置state 状态,不管成功与否都会马上返回;那么 lock.lock() 这种阻塞式的锁是如何实现的呢?
有阻塞就需要排队,实现排队必然需要队列
CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)——概念了解就好,不要记
队列中每个排队的个体就是一个 Node,所以我们来看一下 Node 的结构
Node 节点AQS 内部维护了一个同步队列,用于管理同步状态。
当线程获取同步状态失败时,就会将当前线程以及等待状态等信息构造成一个 Node 节点,将其加入到同步队列中尾部,阻塞该线程
当同步状态被释放时,会唤醒同步队列中“首节点”的线程获取同步状态
为了将上述步骤弄清楚,我们需要来看一看 Node 结构 (如果你能打开 IDE 一起看那是极好的)
乍一看有点杂乱,我们还是将其归类说明一下:
上面这几个状态说明有个印象就好,有了Node 的结构说明铺垫,你也就能想象同步队列的接本结构了:
前置知识基本铺垫完毕,我们来看一看独占式获取同步状态的整个过程
独占式获取同步状态故事要从范式lock.lock() 开始
public void lock() { // 阻塞式的获取锁,调用同步器模版方法,获取同步状态 sync.acquire(1); }进入AQS的模版方法 acquire()
public final void acquire(int arg) { // 调用自定义同步器重写的 tryAcquire 方法 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }首先,也会尝试非阻塞的获取同步状态,如果获取失败(tryAcquire返回false),则会调用 addWaiter 方法构造 Node 节点(Node.EXCLUSIVE 独占式)并安全的(CAS)加入到同步队列【尾部】
private Node addWaiter(Node mode) { // 构造Node节点,包含当前线程信息以及节点模式【独占/共享】 Node node = new Node(Thread.currentThread(), mode); // 新建变量 pred 将指针指向tail指向的节点 Node pred = tail; // 如果尾节点不为空 if (pred != null) { // 新加入的节点前驱节点指向尾节点 node.prev = pred; // 因为如果多个线程同时获取同步状态失败都会执行这段代码 // 所以,通过 CAS 方式确保安全的设置当前节点为最新的尾节点 if (compareAndSetTail(pred, node)) { // 曾经的尾节点的后继节点指向当前节点 pred.next = node; // 返回新构建的节点 return node; } } // 尾节点为空,说明当前节点是第一个被加入到同步队列中的节点 // 需要一个入队操作 enq(node); return node; } private Node enq(final Node node) { // 通过“死循环”确保节点被正确添加,最终将其设置为尾节点之后才会返回,这里使用 CAS 的理由和上面一样 for (;;) { Node t = tail; // 第一次循环,如果尾节点为 null if (t == null) { // Must initialize // 构建一个哨兵节点,并将头部指针指向它 if (compareAndSetHead(new Node())) // 尾部指针同样指向哨兵节点 tail = head; } else { // 第二次循环,将新节点的前驱节点指向t node.prev = t; // 将新节点加入到队列尾节点 if (compareAndSetTail(t, node)) { // 前驱节点的后继节点指向当前新节点,完成双向队列 t.next = node; return t; } } } }你可能比较迷惑 enq() 的处理方式,进入该方法就是一个“死循环”,我们就用图来描述它是怎样跳出循环的
有些同学可能会有疑问,为什么会有哨兵节点?