AQS AbstractQueuedSynchronizer,多线程同步器,是 J.U.C 包中多个组件的底层实现,如 Lock、CountDownLatch、Semaphore 等都用到了它。
双向链表的特点是有两个指针,一个指针指向前置节点,一个指针指向后驱节点,所以双向链表可以支持常量级别的时间复杂度情况下找到前置节点。基于这个特点,双向链表在插入和删除的时候要比单向链表更加简单和高效,那么根据AQS特点,AQS使用双向链表是从3个方面考虑的:
一、没有竞争到锁的线程加入到阻塞队列,并且阻塞等待的一个前提是当前线程所在的节点的前置节点是一个正常状态。这种设计的目的在于为了避免在链表中存在异常状态的节点,导致无法唤醒后续线程的问题,所以线程在阻塞之前,需要去判断前置节点的状态,如果没有指针指向前置节点,那么就需要从头部节点开始往下遍历,这样实现的话,性能是非常低的。
二、在lock接口里面,还有一个可以中断的锁的竞争方法,即lockInterruptibly(),这个方式是表示,处于锁阻塞的线程是允许被中断的,也就是说没有竞争到锁的线程,加入到同步等待队列等待以后,是允许被外部线程通过interrupt()方法去触发唤醒并且中断的,而这个时候被中断的线程的状态会修改成cancelled的状态,被标记为cancelled状态的线程是不需要进行竞争锁的,但它仍然会存在于整个双向链表中,意味着后续的锁的竞争中,需要把这个节点从链表中移除掉,否则会导致阻塞的线程无法被正常唤醒。在这种情况下,如果是单向链表,就需要从head节点,开始往下去逐个遍历,找到并移除异常状态的节点,那么同样效率也是比较低的,而且还会导致锁的唤醒操作和遍历操作之间的竞争。
三、为了避免线程阻塞和唤醒的开销,所以加入到链表的线程,首先会通过自旋的方式去尝试竞争锁,但实际上按照公平锁的设计思想,只有头节点的下一个节点,才有必要去竞争锁,后续的节点去竞争锁的意义不大,否则会造成惊群效应,即大量线程在阻塞之间尝试竞争锁,带来一个比较大的性能开销,所以为了避免这个问题,加入到链表中的节点,在尝试竞争锁的时候,需要去判断前置节点是否是头节点,如果不是头节点,就没有必要触发锁的竞争动作,所以这里会涉及一个前置节点的查找,如果是单向链表,是无法实现这样一个功能的。