Java中的并发包大家应该或多或少都知道。说到并发包,就不得不提到今天要说的AbstractQueuedSynchronizer,简称AQS。这是很多并发工具类的实现基础publicabstractclassAbstractQueuedSynchronizerextendsAbstractOwnableSynchronizerimplementsjava.io.Serializable类,顾名思义,是一个抽象的队列式同步器。AQS定义了一组用于多线程访问共享资源的同步器框架。很多同步类的实现都依赖于它,比如常用的ReentrantLock、Semaphore、CountDownLatch对AQS的深入探索我们先来看这张图。图中有颜色的是Method,无色的是Attribution。总的来说,AQS框架分为五层,从上到下,从浅到深,从AQS暴露的API到底层数据有自定义同步器访问时,只需要重写一些需要的方法即可第一层,不需要关注底层的具体实现过程。原因也很简单,就像我们说的一样,这个东西就是一个抽象的同步器,它把加锁和解锁的操作交给具体的实现类自己去实现。就像这样,自定义同步器在进行加锁或解锁操作时,首先会通过第一层的API进入AQS的内部方法,然后通过第二层获取锁。获取锁成功后,直接执行相应的逻辑。对于获取锁失败的过程,进入第三层和第四层的等待队列处理,而这些处理方式都依赖于第五层的基础数据提供层。如果我这样告诉你,你应该很容易理解AQS的实现。学过AQS的同学对这个图的数据结构应该不陌生。state+Node+CLH变体双向队列的核心思想是通过一个volatilestate状态来表示共享资源的状态。如果请求的资源空闲,则将获取共享资源的线程设置为当前有效线程,然后修改状态为锁定状态,其他线程可以及时看到共享资源被占用,其他线程不得直接返回失败,这样会损失这个并发合约交付的效率,所以引入一个双向队列,放置这个双向等待队列。未抢到共享资源的线程完成等待唤醒机制实际上,这个CLH变体在AQS运行中的双向队列中不存放未抢到共享资源的线程,抢到共享资源的线程resources也会作为队列的头节点head有CLH:Craig、Landin和Hagersten队列是一个单向链表。AQS中的队列是CLH变体的虚拟双向队列(FIFO)。AQS通过将每个请求共享资源的线程封装到一个节点中来实现锁的分配。大家应该很容易理解,就是大家一起抢共享资源,抢到的都是有效线程,放在双向队列的头节点。这是Node节点的属性值和含义。让我简要解释一下。waitStatus是节点在队列中的状态,Thread是当前节点的线程,prev和next是前驱指针和后继指针。这里重点是waitStatus属性CANCELLED(1):表示当前节点已经取消调度。当超时或中断时(中断响应的情况下),会被触发变为该状态,进入该状态后的节点不会再发生变化。SIGNAL(-1):表示后继节点正在等待当前节点唤醒。当后继节点加入队列时,前驱节点的状态会更新为SIGNAL。CONDITION(-2):表示节点正在等待Condition。当其他线程调用Condition的signal()方法时,处于CONDITION状态的节点会从等待队列转移到同步队列,等待获取同步锁。PROPAGATE(-3):在共享模式下,前驱节点不仅会唤醒其后继节点,还可能唤醒后继节点。0:新节点入队时的默认状态。由于这个特性,负值表示该节点处于活动等待状态,而正值表示该节点已被取消。所以在源码中很多地方都是用>0和<0来判断节点状态是否正常。同步状态在stateAQS中维护了一个名为state的字段,表示同步状态。由Volatile修饰,用于显示当前的临界资源。锁情况。私有易失性int状态;对于这种状态,AQS也提供了几种方法。这些方法都是final类型,不能修改子类。AQS中有两种加锁方式,一种是共享的,另一种是独占类型,共享类型也很简单,就是通过控制AQS中的state值,state在AQS中是volatile类型,具有可见性,用于记录加锁状态和重入次数,当然不仅仅是重入次数,其实这个状态在不同的实现类中有不同的含义[ReentrantLock]:状态用于记录锁持有状态和重入次数,state=0表示没有线程持有锁;state=1表示有一个线程持有锁;state=N表示线程exclusiveOwnerThread已经重新入锁N次了。[ReentrantReadWriteLock]:state用于记录读写锁的占用状态,持有线程数(读锁),重入次数(写锁)。state的高16位记录持有读锁的线程数,低16位记录写锁线程重入次数,如果这16位的值为0,则表示没有线程占用锁,否则表示有一个线程持有锁。另外,对于读锁,每个线程获得读锁的次数由HoldCounter记录在本地线程变量中。【信号量】:状态,用于计数。state=N表示还有N个信号量可以分配,state=0表示已经没有信号量了。此时,所有需要获取信号量的线程都在等待;[CountDownLatch]:state也是用来计数的,每次countDown减一,减到0时唤醒被await阻塞的线程。记住:volatilestate属性和waitStatus属性有两种区别在Node节点上抢占共享资源:公平锁和非公平锁。用过ReentrantLock的同学肯定知道,默认是非公平锁,但是我们可以传入一个参数,设置为公平锁。根据ReentrantLock,我们来谈谈公平锁和非公平锁。公平锁是公平的,可以保证获取锁的线程按照先到先得的顺序获取锁。不公平的锁。每个线程获取锁的顺序不一定与它们申请锁的顺序相同。后面的线程可能先获取锁。在实现上,当一个公平锁被锁定后,会先执行tryAcquire()操作。在tryAcquire中会判断等待队列中是否还有其他线程在等待。如果队列中已经有其他线程,则tryAcquire失败,它会将自己添加到队列中。如果队列中没有其他线程,则执行获取锁的操作。非公平锁,在加锁的时候,会直接尝试加锁。如果成功,将获得锁。如果失败,它将执行与公平锁相同的操作。从公平锁和非公平锁的实现来看,它们的操作基本相同。唯一不同的是,在加锁的时候,非公平锁会直接先尝试加锁。当前面的线程使用完锁并释放了,此时等待队列不为空,如果这是一个新的线程申请锁,那么公平锁和非公平锁的性能会有所不同。公平锁的优点:线程按顺序获取锁,不会出现饥饿现象(注:饥饿现象是指一个线程的CPU执行时间被其他线程占用,导致没有CPU执行。缺点:整体吞吐效率比较低公平锁比较低,等待队列中除了一个线程外的所有线程都会被阻塞,CPU唤醒线程的开销比非公平锁要大。非公平锁的优点:可以减少调用线程的消耗上下文切换,整体吞吐量比公平锁高。缺点:在高并发环境下可能会出现线程优先级倒置和饥饿。AQS作为并发编程的框架,为其他很多同步工具提供了很好的解决方案。下面列出JUC中的同步工具,大致介绍一下AQS的应用场景:
