AQS 概述
同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了排队和阻塞机制。状态的更新使用getState,setState 以及compareAndSetState 这三个方法。
子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作。
AQS的模板方法设计模式
AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。举个例子,AQS中需要重写的方法tryAcquire :
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
ReentrantLock中NonfairSync(继承AQS)会重写该方法为:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
而AQS中的模板方法acquire():
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被
NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点:
- 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内部类;
- AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法;
- AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义;
- 在重写AQS的方式时,使用AQS提供的getState(),setState(),compareAndSetState() 方法进行修改同步状态。
AQS提供的模板方法可以分为3类:
1. 独占式获取与释放同步状态;
2. 共享式获取与释放同步状态;
3. 查询同步队列中等待线程情况;
独占式锁:
- void acquire(int arg) : 独占式获取同步状态,如果获取失败则插入同步队列进行等待。
- void acquireInterruptibly(int arg) : 与acquire方法相同,但在同步队列中等待时可以响应中断。
- boolean tryAcquireNanos(int arg,long nanosTimeout) : 在2的基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
- boolean tryAcquire(int arg) : 获取锁成功返回true,否则返回false
- boolean release(int arg) : 释放同步状态,该方法会唤醒在同步队列中的下一个节点。
共享式锁:
- void acquireShared(int arg) : 共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态。
- void acquireSharedInterruptibly(int arg) : 增加了响应中断的功能
- boolean tryAcquireSharedNanos(int arg,lone nanosTimeout) : 在2的基础上增加了超时等待功能
- boolean releaseShared(int arg) : 共享锁释放同步状态。
同步队列
当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是通过链式方式进行实现。接下来,很显然我们至少会抱有这样的疑问:
- 节点的数据结构是什么样的?
- 是单向还是双向?
- 是带头结点的还是不带头节点的?
在AQS有一个静态内部类Node,这是我们同步队列的每个具体节点。在这个类中有如下属性
4. volatile int waitStatus; // 节点状态
5. volatile Node prev; // 当前节点的前驱节点
6. volatile Node next; // 当前节点的后继节点
7. volatile Thread thread; // 当前节点所包装的线程对象
8. Node nextWaiter; // 等待队列中的下一个节点
节点的状态如下:
9. int INITIAL = 0; // 初始状态
10. int CANCELLED = 1; // 当前节点从同步队列中取消
11. int SIGNAL = -1; // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继
节点,使得后继节点的线程继续运行。
12. int CONDITION = -2; // 节点在等待队列中,节点线程等待在Condition上,当其他线程对
Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同
步状态的获取中。
13. int PROPAGATE = -3; // 表示下一次共享式同步状态获取将会无条件地被传播下去。
可以知道这样几点:
- 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息;
- 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列;
那么,节点如何进行入队和出队操作?
实际上这对应着锁的获取和释放两个操作:
获取锁失败进行入队操作,获取锁成功进行出队操作。