JUC-CountDownLatch基础篇
JUC-CountDownLatch源码分析
JUC-Semaphore基础篇
JUC-Semaphore源码分析
JUC-ReentrantReadWriteLock锁基础篇
JUC-ReentrantReadWriteLock锁源码分析
JUC-ReentrantLock锁基础篇
JUC-ReentrantLock锁源码分析
JUC-CyclicBarrier基础篇
JUC-CyclicBarrier源码分析
文章目录
- 1.什么是AQS?
- 2.AQS架构图
- 3.AQS的state值
- 4.AQS的同步队列
-
- AQS提供了两种锁机制,分别是排它锁和共享锁
- 5.AQS的Node节点的waitStatus变量
- 6.AQS的Propagate传播动作
- 7.AQS的条件队列
1.什么是AQS?
AQS全称是AbstractQueuedSynchronizer,这个抽象类是java并发包java.util.concurrent.locks(JUC)中的核心,它是JUC包下面多个组件的底层实现。比如CountDownLatch,Semaphore,CyclicBarrier,ReentrantLock,ReentrantReadWriteLock,ThreadPoolExecutor等的底层实现都用到了AQS。简单来讲,AQS是一个抽象类,这些组件的共性功能,它实现了,差异化的功能,它提供接口,由具体的组件来实现。AQS定义了模板,具体实现由各个子类完成。
AQS是一个多线程同步器,是用来进行多线程之间的并发控制的。并发控制的核心是锁的获取与释放。那么我们怎么知道这个锁有没有被别人获取呢?也就是说要有一个东西来记住锁是被别人获取了还是没有获取,于是AQS就有一个变量state,来代表锁有没有被获取,锁能不能用。但是当锁已经被别的线程获取了,此时获取锁的线程,总不能把它扔了吧,所有此时就需要有一个队列,来存在这些因为获取锁而阻塞住的线程,以等待后续的被唤醒。
2.AQS架构图
实现Serializable接口,继承了一个AbstractOwnableSynchronizer父类。AbstractOwnableSynchronizer父类中维护了一个exclusiveOwnerThread属性,是用来记录当前同步器资源的独占线程的,就没有其他的了。
3.AQS的state值
一个线程能不能竞争到锁其实就是体现在通过CAS操作能不能成功的将state的值改变。释放锁也是将state的值改变。
AQS的核心思想是:通过一个volatile修饰的int类型的state来代表同步状态。例如,0是无锁状态,1是上锁状态,当多线程竞争资源时,通过CAS的方式将,来尝试的将state的状态从0修改为1.如果修改成功,则代表这个线程获取锁成功。此时state值变成1(注意:因为state是用volatile修饰的,保证了内存可见性。也就是说一个线程修改了state值,例外一个线程是能够看的见这种修改的)。如果修改失败了,说明获取锁失败,则进入同步队列中等待。等前面获取了锁的线程释放锁之后,会将state的值由1修改成0,这个获取了锁的线程会将这个同步队列中head头结点的下一个线程唤醒,让这个阻塞的线程由阻塞状态唤醒过来继续去竞争获取锁。
4.AQS的同步队列
AQS的同步队列其实就是一个FIFO的队列,这个队列的内部元素类型是一个AQS的内部类Node类型。AQS会将竞争锁失败的线程封装成一个Node节点,然后由这些Node节点组成了一个双向的链表队列(队列包含一个head指针和tail指针,head指针指向一个没有任何意义的头部节点,tail指针指向最后一个加入到同步队列的节点)。一个Node节点包含了当前被阻塞的线程,node节点的状态值、是独占还是共享模式以及它的前驱和后继节点等信息。
Node节点数据结构:
static final class Node {
/** 作为共享模式 */
static final Node SHARED = new Node();
/** 作为独占模式 */
static final Node EXCLUSIVE = null;
/** 等待状态:表示节点中线程是已被取消的 */
static final int CANCELLED = 1;
/** 等待状态:表示当前节点的后继节点的线程需要被唤醒 */
static final int SIGNAL = -1;
/** 等待状态:表示线程正在等待条件 */
static final int CONDITION = -2;
/**
* 等待状态:表示下一个共享模式的节点应该无条件的传播下去
*/
static final int PROPAGATE = -3;
/**
等待状态,初始化为0,后续变化赋值为以上等待状态的值
*/
volatile int waitStatus;
/**
同步队列中当前节点的前驱节点
*/
volatile Node prev;
/**
同步队列中当前节点的后继节点
*/
volatile Node next;
/**
当前节点的线程,即被阻塞住的线程
*/
volatile Thread thread;
/**
条件队列中表示当前节点的后继节点
同步队列中表示当前节点的模式 共享模式or独占模式(SHARED or EXCLUSIVE)
*/
Node nextWaiter;
/**
是否是共享节点
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
当前节点的前驱节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
// Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) {
// Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
// Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS提供了两种锁机制,分别是排它锁和共享锁
AQS定义两种资源共享方式即EXCLUSIVE(独占)模式和SHARED(共享)模式,也就是分别对应了排它锁与共享锁,体现在Node节点中就是waitStatus 变量,waitStatus=Exclusive代表此节点是一个独占模式的节点,waitStatus=Share代表此节点是一个共享模式的节点。
共享模式允许多个线程可以同时获取同一个锁,独占模式则一个锁只能被一个线程持有,其他线程必须要等待。
同步队列的入队:
当一个线程竞争锁失败时,就会新建一个Node节点出来,Node节点的thread变量的值就是此线程。如果竞争的锁是排它锁Node节点的nextWaiter变量的值是EXCLUSIVE,如果竞争的锁是共享锁Node节点的nextWaiter变量的值是SHARED。然后,如果此时同步队列还没有初始化。那就会进行同步队列的初始化操作,即新建一个Node出来,然后head(waitStatus=0)指针和tail指针都指向这个节点。队列初始化完成后,就会将前面新建出来的Node节点加入到队列中。同时将此节点的前驱节点的waitStatus值改为-1。也就是说head节点此时的值变成了-1,tail尾指针指向这个新建的节点。
一个新建节点的入队操作流程:先新建好一个Node节点。然后将此新建好的Node节点以尾加入的方式入队,同时将它前驱节点的waitStatus值改为-1,tail指针指向自己。
同步队列的出队:
当持有锁的线程进行了释放锁的操作时,会将同步队列head节点的后继节点进行唤醒,然这个后继节点重新去尝试竞争这把锁。
注意:是尝试的竞争,就代表不一定一定能竞争到这把锁。假如此时又有一个新的线程来了,也来竞争这把锁,那么到底是谁能够竞争到这把锁呢。此时就有了公平锁和非公平锁的概念。
公平锁: 如果采用的是公平策略的话,那么就是这个被唤醒的线程来获取这把锁,新来的那个线程老老实实执行入队操作等着被唤醒。
非公平锁: 如果采用的是非公平策略的话,那么就是这个被唤醒的线程和新来的线程一起来获取这把锁。谁竞争到了就算谁的。
一个节点出队的操作流程:当有线程执行了释放锁操作的时候,就会从同步队列中唤醒阻塞的节点。那么唤醒的是哪一个节点呢?永远是head节点的后继节点。如果head节点的waitStatus值是-1的话(注意如果一个节点入队了,那么它的前驱节点的waitStatus值一定是-1,除非这个前驱节点因为某些原因别取消了,比如线程中断,那么这个前驱节点的waitStatus值会变成1。但是这个取消的节点会被踢出同步队列,所有不用担心自己的前驱节点的waitStatus不是-1,而导致自己不能别唤醒的情况发生),就将head节点的后继节点唤醒,当这个后继节点竞争到锁了后,同时将head节点的waitStatus赋值给0,并且重新将head指针指向自己,,此时自己就成为了head节点。此时队列就少了一个节点了。也就是一个节点出队了。那么被唤醒的节点竞争到了锁以后还会不会干点其他的事情呢?这就分情况了,如果是竞争到的锁是排他锁,那么只会执行自己的业务逻辑,执行完了之后就会释放锁,继续唤醒同步队列的节点。如果竞争到的锁是共享锁的话,那么除了执行自己的业务逻辑外,如果自己的后继节点是共享模式的话,那么就会去唤醒自己的后继节点。因为共享锁是可以由多个线程共同持有的,而排他锁只能由一个线程持有。那么有人就问,为什么要这个被唤醒的节点去唤醒它的后继节点,等这个节点执行完自己的业务逻辑执行释放锁的操作,不一样也可以释放自己的后继节点吗?这就涉及到了尽快唤醒被阻塞节点的原则了。也就是说,被阻塞住的节点能尽快的唤醒就尽快的唤醒。那么一个被唤醒的节点,获取到了共享锁,然后发现它的后继节点也是要获取这把共享锁(这个后继节点的nextWaiter=SHARED可以表明这个节点获取的锁是共享锁),那么立马去唤醒它,是不是比等我释放了锁再去唤醒它要快很多呢。
同步队列中的节点是不是只有由释放锁的线程来唤醒呢?
不是,当阻塞住的线程发生了线程中断时,也会被唤醒。至于因为中断而被唤醒的节点,接下来要干啥就不同AQS实现有不同的具体去干啥了。
如果一个节点被取消了waitStatus=1,此时刚好有唤醒操作来唤醒它,此时怎么办?
如果这个节点被取消了,此时刚好有唤醒的操作过来,来唤醒它。因为它已经被取消了,所有不需要这个操作来唤醒它,那么这个唤醒操作不能不敢什么事情吧,此时就是从同步队列的tail指针向前找一个有效节点再唤醒。注意:一个被取消的节点通常情况下会被踢出同步队列,但是也存在还没来的及把它踢出去,而发生了这种情况。
5.AQS的Node节点的waitStatus变量
同步队列中Node节点的waitStatus变量取值有0,1,-1,-3。
当一个Node节点刚刚创建出来的时候,它的waitStatus值为0。当一个新来的Node节点要入队列的时候,它会把它的前驱Node节点的waitStatus值改成-1。当一个在同步队列中的节点因为某种原因被取消了(比如这个节点被线程中断了),那么它的值变成了1.这个被取消的节点,会因为有新的节点入队列或者有节点被唤醒等一些操作而别提出同步队列中。而waitStatus=-3,这种情况就涉及到了Propagate传播动作了。
6.AQS的Propagate传播动作
AQS的Propagate传播特性只存在与共享锁中,也就是这个锁是共享锁才有这个动作,排他锁是没有的。那么这个传播动作是啥意思?
如果一个线程释放了一个把共享锁,那么它会唤醒head节点的后继节点。同时也告诉这个后继节点,我释放的是共享锁呀,如果你的后继节点需要的锁也是共享锁的话,那么你就去释放它,同时这个后继节点也告诉它的后继节点,我释放的是共享锁呀,如果你的后继节点需要的锁也是共享锁的话,那么你就去释放它。就这样一个节点一个节点的把共享锁被释放的信息传播下去。这就是传播动作。除非遇到一个节点,它要获取的锁是排它锁,这个传播动作才终止。
那么这个传播特性在代码中该怎么表示呢?两种方式一个是通过setHeadAndPropagate(Node node, int propagate)方法的propagate来表示,如果propagate>0的话就代表有传播动作。(注意:释放共享锁时,被唤醒的线程竞争到了这把共享锁后会调用这个方法,如果需要传播动作就将propagate传一个大于0的值),还有一个是head节点的waitStatus=-3来表示。然后head节点什么时候才会变成-3呢?前面说过如果来了一个释放锁的操作。那么就会将head节点的后继节点唤醒,并把head节点的waitStatus值赋值成0,并且将这个后继节点设置成新的head节点。那么在将head的waitStatus值赋值成0,并且还没来的及将head指针指向这个后继节点时,又来了一个唤醒操作,改怎么办。前面说过只有head节点的waitStatus值为-1,这个唤醒操作才会去唤醒同步队列中的节点。但是此时head节点的waitStatus值为0。那么这个新来的唤醒操作不就丢失了吗?此时的做法是将这个head节点的waitStatus赋值成-3。当前面的head指针指向了后继节点完成后,这个后继节点变成了新的头节点,会去检查之前的旧的头节点。如果发生旧的头结点的waitStatus小于0,如果这个后继节点自己的后继节点也是需要获取共享锁时,就会去唤醒自己的后继节点。这就保证了那个新来的唤醒操作不会丢失了。
7.AQS的条件队列
我们前面说到了一个线程竞争不到锁就会进行同步队列中等待那么这个条件队列又是干啥的?
首先条件队列是需要手动明确的创建出来的,不想同步队列那样,同步队列是竞争不到锁的线程进入同步队列中,发现同步队列没有创建的话,就会先进行初始化的。而条件队列是需要调用newCondition()方法来返回一个Condition对象。AQS内部有一个内部类ConditionObject,newCondition()方法返回的实际上是这个内部类
目前只有ReentrantLock锁和ReentrantReadWriteLock的写锁可以调用newCondition()方法来创建出条件队列。也就是说条件队列是排它锁(独占锁)独有的,共享锁没有。
**条件队列的结构:**是一个单向的链表队列,队列元素类型也是Node类型。有两个指针firstWaiter指向队列第一个节点,lastWaiter指向队列最后一个节点。Node节点的nextWaiter指向下一个节点。新建一个Node节点入队时,Node节点的waitStatus值为-2。
条件队列的入队: 调用Condition对象的await方法。新建一个Node节点。如果条件队列的lastWaiter指针为null,说明还没有初始化,此时firstWaiter,lastWaiter指针都指向这个新建的Node节点。否则新建的节点尾方式入队,lastWaiter指针指向这个新建的Node节点。同时调用fullyRelease方法唤醒同步队列中阻塞的节点,然后调用isOnSyncQueue方法判断这个新建的Node节点在不在同步队列上,如果不在,则阻塞住这个线程。
条件队列的出队: 调用Condition对象的signal方法,会将firstWaiter指针指向的Node节点从条件队列中移出,将firstWaiter指针指向此Node节点的下一个节点。同时这个被移出的节点加入到了同步队列中。当这个加入到同步队列中的节点被唤醒时,会接着调用acquireQueued方法来继续竞争这把锁。
注意:条件队列出队的节点加入到了同步队列中去了
条件队列Condition的使用及场景举例可以参考JUC-ReentrantReadWriteLock锁基础篇