前面的文章中又介绍的在争用激烈的场景下,使用基于CAS自旋实现的轻量级锁有两个大的问题:
(1)CAS恶性空自旋会浪费大量的CPU资源。
(2)在SMP架构的CPU上会导致“总线风暴”。
解决CAS恶性空白旋的有效方式之一是以空间换时间,较为常见的方案有两种:分散操作热点和使用队列削峰。JUC并发包使用的是队列削峰的方案解决CAS 的性能问题,并提供了一个基于双向队列的削峰基类、抽象基础类AbstractQueuedSynchronizer(抽象同步器类,简称为AQS)。
锁与队列的关系
无论是单体服务应用内部的锁,还是分布式环境下多服务应用所使用的分布式锁,为了减少由于无效争夺导致的资源浪费和性能恶化,一般都基于队列进行进行排队与削峰
CLH锁的内部队列
CLH(Craig,Landin,and Hagersten lock Queue)是一个单向队列,也是一个FIFO队列。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问,队列头部的节点表示占有锁的节点,新加入的抢锁线程则需要等待,会插入队列的尾部
CLH锁的内部结构如图
分布式锁的内部队列
在分布式锁的实现中,比较常见的是基于队列的方式进行不同节点中“等锁线程”的统一调度和管理。以基于 Zookeeper 的分布式锁为例,其等待队列的结构大致如图所示
AQS 的内部队列
AQS是JUC提供的一个用于构建锁和同步容器的基础类。JUC包内许多类都是基于AQS构建的,例如ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。
AQS是CLH队列的一个变种,主要原理和CLH队列差不多,这也是前面对CLH队列进行长篇大论介绍的原因。AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的前驱节点和直接的后继节点。所以双向链表可以从任意一个节点开始很方便地访问前驱节点和后继节点。每个节点其实是由线程封装的,当线程争抢锁失败后会封装成节点加入AQS队列中;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
AQS的内部结构如图所示