目录
1、前言
俗话说得好,学会AQS就学会了JUC。今天我们就来了解一下,这个传说中的AQS的内部结构。
(强烈建议英语好的同学直接看AQS的注释!!)
2、AQS的构成
抽象队列同步器(即AQS),通过一系列模板办法为我们实现JUC下各种各样的工具锁提供了基础,通过继承它,我们可以很轻松的实现一把自己的锁。
假如是让我们来实现锁的逻辑,结合synchronized的实现原理,我们很容易得出,我们需要:
- 表示线程是否被上锁的状态信息
- 线程竞争失败时需要存放的队列
- 能让线程进行阻塞/唤醒
有了这三个东西,我们就可以保证线程通过修改可见变量的状态来标识是否被占用,以及上锁失败时线程应该阻塞的地方。
当然,为了知道当前是哪个线程得到了锁资源,我们还需要一个变量来表明当前持锁线程:exclusiveOwnerThread
。
2.1 状态信息state
在AQS中,使用了一个被volatile修饰的int类型变量来标识状态信息。
/**
* The synchronization state.
*/
private volatile int state;
在使用过程中,我们可以通过定义volatile的状态来实现不同的逻辑。比如:
- ReentrantLock中将state=0表示无锁状态,state>0表示加锁状态。如state=3就代表,有个线程已经持有锁,并且重入了两次。
- CountDownLatch中则是在构造器中定义state的大小来实现计数器的功能
- Semaphore中也是通过定义state的大小与修改state来实现限流器的功能
通过CAS来修改state的状态,AQS不用申请monitor就能保证线程安全,同时还能实现各种各样不同的功能。
2.2 节点信息Node
线程竞争失败,自然需要在一个地方阻塞等待条件成立。但是直接把Thread放进去队列似乎有点不太合适,既然是队列,就得有节点。于是AQS需要把线程封装成节点信息之后再放进去,这里贴一下Node的成员变量:
/**
* 标记节点未共享模式
* */
static final Node SHARED = new Node();
/**
* 标记节点为独占模式
*/
static final Node EXCLUSIVE = null;
/**
* 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
* */
static final int CANCELLED = 1;
/**
* 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
* 将会通知后继节点,使后继节点的线程得以运行。
*/
static final int SIGNAL = -1;
/**
* 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
* 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会被无条件地传播下去
*/
static final int PROPAGATE = -3;
/**
* 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
* 即被一个线程修改后,状态会立马让其他线程可见。
*/
volatile int waitStatus;
/**
* 前驱节点,当前节点加入到同步队列中被设置
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 节点同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量,
* 也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段。
*/
Node nextWaiter;
这里我们关注三个点:
- 前驱节点: volatile Node prev;
- 后继节点:volatile Node next;
- 节点同步状态的线程:volatile Thread thread;
在关于AQS竞争与释放流程剖析的时候,这几个东西可是十分重要的。
2.3 阻塞/唤醒
其实这里没有什么好说的,这里AQS使用了LockSupport工具类中的方法,而再进一步就是使用了unsafe中的park和unpark方法。
如果还要进一步就是涉及到由C语言实现的_count(是否阻塞)、_cond(条件变量)、_mutex(互斥锁)。
2.4 当前持有锁线程
在AQS中并没有直接使用一个变量来标识当前持有锁的线程,而是选择继承了另一个抽象类:AbstractOwnableSynchronizer
。
在这个类中,主要就是围绕当前持有锁线程变量提供了get/set操作。
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
/**
* 独占模式同步器的当前持有线程.
* transient关键字表示属性不参与序列化
*/
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
2.5 头结点与尾节点
关于这个,会在AQS多线程下获取与释放流程剖析时进行讲解。
/**
* 指向同步等待队列的头节点
*/
private transient volatile Node head;
/**
* 指向同步等待队列的尾节点
*/
private transient volatile Node tail;
3、CLH队列
CLH队列也是AQS的组成部分。之所以把它单独拎出来讲,是因为CLH是一种算法,而且AQS中是CLH队列的变种。同时CLH队列的优化还有MCS。
在讲CLH队列之前,我们先了解两个前置知识点(我抄来的):
-
SMP(Symmetric Multi-Processor)
对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。
SMP能够保证内存一致性,但这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,
可能会导致CPU资源的浪费。常用的PC机就属于这种。 -
NUMA(Non-Uniform Memory Access)
非一致存储访问,将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块相互访问,
访问本地内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问的由来。NUMA较好地解决SMP的扩展问题,
当CPU数量增加时,因为访问远地内存的延时远远超过本地内存,系统性能无法线性增加。
3.1 CLH锁
在基础的自旋锁算法下,为了保证公平锁,CLH队列中的申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
CLH队列锁是一种基于链表的可扩展、高性能、公平的自旋锁。优点是空间复杂度低,但是在NUMA系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,则性能会大打折扣。
3.2 MCS锁
为了应对这种情况,MCS锁修改了自旋的规则:CLH是在前趋结点的locked域上自旋等待,而MCS是在自己的结点的locked域上自旋等待。正因为如此,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题
3.3 AQS中的变种CLH锁
AQS在CLH的基础上进行了变种:CLH是单向队列,其主要特点是自旋检查前驱节点的locked状态。而AQS同步队列是双向队列,每个节点也有状态waitStatus,而其并不是一直对前驱节点的状态自旋,在尝试自旋一次后会将线程阻塞让出CPU时间片,等待前驱节点主动唤醒自己。
4、结尾
事实上这一章都是为了下一章的AQS多线程下获取与释放流程剖析做铺垫,对AQS有了大概的了解,我们在下一章就不至于那么懵逼~