前言
-
如果你想深入研究Java并发的话,那么AQS一定是绕不开的一块知识点,Java并发包很多的同步工具类底层都是基于AQS来实现的
-
比如我们工作中经常用的Lock工具ReentrantLock、栅栏CountDownLatch、信号量Semaphore等,而且关于AQS的知识点也是面试中经常考察的内容
-
ReentrantLock
-
ReentrantReadWriteLock
-
CountDownLatch
-
Semaphore
-
SynchronousQueue
-
FutureTask
-
-
所以,无论是为了更好的使用还是为了应付面试,深入学习AQS都很有必要。
-
-
CAS机制:
-
CAS是乐观锁的一种思想,它假设线程对资源的访问是没有冲突的,同时所有的线程执行都不需要等待,可以持续执行。
-
如果有冲突的话,就用比较+交换的方式来检测冲突,有冲突就不断重试。
-
-
++CAS原理++:
-
CAS的全称是Compare-and-Swap,也就是比较并交换
-
它包含了三个参数:V,A,B,V表示要读写的内存位置,A表示旧的预期值,B表示新值,当执行CAS时,只有当V的值等于预期值A时,才会把V的值改为B,
-
这样的方式可以让多个线程同时去修改,但也会因为线程操作失败而不断重试,对CPU有一定程序上的开销。
-
AQS简介
-
AQS,全名AbstractQueuedSynchronizer,是一个抽象类的队列式同步器
-
它的内部通过维护一个状态volatile int state(共享资源)
-
和一个FIFO线程等待队列来实现同步功能。
-
-
state用关键字volatile修饰,代表着该共享资源的状态一更改就能被所有线程可见
-
而AQS的加锁方式本质上就是多个线程在竞争state
-
当state为0时代表线程可以竞争锁
-
不为0时代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中
- 这些线程会被 UNSAFE.park() 操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。
-
-
而这个等待队列其实就相当于一个CLH队列,用一张原理图来表示大致如下:
基础定义
-
AQS支持两种资源分享的方式:
-
Exclusive(独占,只有一个线程能执行,如ReentrantLock)
-
Share(共享,多个线程可同时执行,如Semaphore、CountDownLatch)。
-
-
自定义的同步器继承AQS后,只需要实现共享资源state的获取和释放方式即可
- 其他如线程队列的维护(如获取资源失败入队/唤醒出队等)等操作,AQS在顶层已经实现了
方法
-
AQS代码内部提供了一系列操作锁和线程队列的方法,主要操作锁的方法包含以下几个:
-
compareAndSetState():
- 利用CAS的操作来设置state的值
-
tryAcquire(int):
-
独占方式获取锁。
-
成功则返回true;
-
失败则返回false。
-
-
tryRelease(int):
-
独占方式释放锁。
-
成功则返回true;
-
失败则返回false。
-
-
tryAcquireShared(int):
-
共享方式释放锁。
-
负数表示失败;0表示成功,但没有剩余可用资源;
-
正数表示成功,且有剩余资源。
-
-
tryReleaseShared(int):
-
共享方式释放锁。
-
如果释放后允许唤醒后续等待结点返回true,否则返回false。
-
-
-
像ReentrantLock就是实现了自定义的tryAcquire-tryRelease,从而操作state的值来实现同步效果。
内部Node节点
-
除此之外,AQS内部还定义了一个静态类Node,表示CLH队列的每一个结点,该结点的作用是对每一个等待获取资源做了封装,包含了需要同步的线程本身、线程等待状态…
-
我们可以看下该类的一些重点变量:
static final class Node { /** 表示共享模式下等待的Node */ static final Node SHARED = new Node(); /** 表示独占模式下等待的mode */ static final Node EXCLUSIVE = null; /** 下面几个为waitStatus的具体值 */ static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; /** 表示前面的结点 */ volatile Node prev; /** 表示后面的结点 */ volatile Node next; /**当前结点装载的线程,初始化时被创建,使用后会置空*/ volatile Thread thread; /**链接到下一个节点的等待条件,用到Condition的时候会使用到*/ Node nextWaiter; }
waitStatus
-
代码里面定义了一个表示当前Node结点等待状态的字段waitStatus,该字段的取值包含了CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0,这五个值代表了不同的特定场景:
-
CANCELLED:表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
-
SIGNAL:表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL(记住这个-1的值,因为后面我们讲的时候经常会提到)
-
CONDITION:表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。(注:Condition是AQS的一个组件,后面会细说)
-
PROPAGATE:共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
-
0:新结点入队时的默认状态。
-
-
也就是说,当waitStatus为负值表示结点处于有效等待状态,为正值的时候表示结点已被取消。
-
在AQS内部中还维护了两个Node对象head和tail,一开始默认都为null
private transient volatile Node head; private transient volatile Node tail;
总结
-
讲完了AQS的一些基础定义,我们就可以开始学习同步的具体运行机制了
-
为了更好的演示,我们用ReentrantLock作为使用入口,一步步跟进源码探究AQS底层是如何运作的
-
这里说明一下,因为ReentrantLock底层调用的AQS是独占模式,所以下文讲解的AQS源码也是针对独占模式的操作
-
好了,热身正式结束,来吧。