J.U.C之AQS:AQS详解和使用
1 AQS是什么
AQS是AbstractQueuedSynchronizer的缩写,翻译过来就是"同步器",AbstractQueuedSynchronizer是一个抽象类,它实现了Java函数中锁同步(synchronized),锁等待(wait,notify)功能。
Java并包里大部分并发工具类都将其作为核心基础构件,比如可重入锁ReentrantLock, 信号量Semaphore基于各自的特点来使用AQS提供的基础能力方法实现多线程交互。
2 AQS核心功能
锁同步(synchronized)
锁等待(wait,notify)
3 AQS 中概念
3.1 同步状态
AQS实现了锁,必然需要一个竞争对象。AQS存在从一个int类型的成员变量state,我们把它称为同步状态,基于开闭原则,内部提供了很多模板方法【参考AQS核心方法】给子类去定制如何获取释放同步状态。
3.2 获取同步状态方式
AQS按照获取释放同步状态的方式分为"独占式同步","共享式同步"。
独占式同步
从概念上来说独占式对应只存在一个资源,且只能被一个线程或者说竞争者占用.
共享式同步
从概念上来说共享式对应存在多个资源的是有多个线程或者竞争者能够获取占用.
3.3 同步队列
AQS 实现了锁那么总需要一个队列将无法获取锁的线程保存起来,方便在锁释放时通知队列中线程去重新竞争锁。
实现原理
同步队列又被称为CLH同步队列,CLH队列是通过链式方式实现FIFO双向队列。当线程获取同步状态失败时,AQS则会将当前线程构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态被释放时,会把首节点后第一个节点的线程从阻塞状态下唤醒,唤醒的线程会尝试竞争同步状态,如果能获取同步状态成功,则从同步队列中出队。
实例
public class LockDemo {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(() -> {
lock.lock();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread.start();
}
}
}
实例代码中开启了5个线程,先获取锁之后再睡眠10S中。通过debug,当Thread-4(在本例中最后一个线程)获取锁失败后进入同步时,AQS时现在的同步队列如图所示
3.4 Condition & 等待队列
Java 传统的监视器有如下函数 wait、notify、notifyAll。它们可以实现当一个线程获取锁时,它可以主动放弃锁进入一个条件队列中。只有其他线程通知时才从条件队列中出队,重新获取锁成功后继续执行之前的未完成代码逻辑。
AQS内部存在一个内部类实现了Condition接口,其内部维护着一条链式实现单向等待队列。我们可以使用AQS获取内部实现Condition接口对象,调用await(),signal(),signalAll()函数实现Java中wait、notify、notifyAll同样功能。
实现原理
- 当获取同步状态的线程调用condition.await(),则会阻塞,并进入一个等待队列,释放同步状态.
- 当其他线程调用了condition.signal()方法,会从等待队列firstWaiter开始选择第一个等待状态不是取消的节点.添加到同步队列尾部.
- 当其他线程调用了condition.signalAll()方法,会从等待队列firstWaiter开始选择所有等待状态不是取消的节点.添加到同步队列尾部.
这里取消节点表示当前节点的线程不在参与排队获取锁。
实例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
lock.lock();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
});
thread.start();
}
}
新建了10个线程,每个线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下:
4 AQS 实现原理
AQS核心是一个同步状态,两个队列。它们实现了Java函数中锁同步(synchronized),锁等待(wait,notify),并在其基础上实现了独占式同步,共享式同步2中方式锁的实现。
无论独占式还时共享式获取同步状态成功则直接返回,失败则进入CLH同步队列并阻塞当前线程。当获取同步状态线程释放同步状态,AQS会选择从CLH队列head头部节点的第一个节点释放阻塞,尝试重写竞争获取同步状态,如果成功则将当前节点出队。如果失败则继续阻塞。
获取同步状态的线程也可以使用condition对象释放同步状态进入等待队列。只有等待其他线程使用condition.signal或condition.signAll()唤醒被从阻塞状态中释放重新竞争获取同步状态成功后从原来指令位置继续运行。
5 AQS核心方法
5.1 独占式同步
acquire
获取同步状态,如果当前线程获取同步状态成功则直接返回,如果获取失败插入同步队列尾部,同时线程阻塞,当调用release释放同步状态时,会从同步队列head头部后第一个节点中线程从阻塞中释放并在自旋中重新竞争同步状态,如果获取成功则从同步队列出队,并返回,如果获取失败则继续阻塞,等待下次唤醒。
acquireInterruptibly
独占式获取同步状态,与acquire方法相同,但在如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;
tryAcquireNanos
独占式获取同步状态,与acquireInterruptibly方法相同,但在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false
release
释放独占式同步状态,唤醒同步队列中首节点之后的第一个等待节点的线程的阻塞。
5.2 共享式同步
acquireShared
获取同步状态,如果当前线程获取同步状态成功则直接返回,如果获取失败插入同步队列尾部,同时线程阻塞。当调用releaseShared释放同步状态时,会找到从head头部节点后置节点中的线程,并将该线程从阻塞中释放。被释放的线程会在自旋中重新竞争同步状态。如果获取成功则出队,同时会释放后置节点中的线程从阻塞中唤醒竞争同步状态。
在acquireShared方法基础上增加了能响应中断的功能;
tryAcquireSharedNanos
在acquireSharedInterruptibly基础上增加了超时等待的功能;
releaseShared
释放共享式同步状态,释放共享式同步状态会唤醒同步队列中首节点之后的第一个等待节点的线程。
5.3 同步状态
getState()
返回同步状态的当前值
setState(int newState)
设置当前同步状态
compareAndSetState(int expect, int update)
使用CAS设置当前状态,该方法能够保证状态设置的原子性
5.4 同步队列
hasQueuedThreads()
查询是否有任何线程正在等待获取。【在同步队列中是否存在等待线程】
int getQueueLength()
返回等待获取的线程数的估计值.在同步队列中是否存在等待线程数量】
getQueuedThreads()
返回包含可能等待获取的线程的集合。因为实际的线程集可能在构造此结果时动态地改变,所以返回的集合仅是尽力而为的估计值【返回同步队列中线程集合】
6 模板方法
我们可以编写自己类继承AQS选择重写独占式或共享式模板方法,从而定义如何获取同步状态和释放同步状态的逻辑。
6.1 独占式
tryAcquire:
尝试独占式获取同步状态,返回值为true则表示获取成功,否则获取失败。
调用场景:
- 1 在acquire开始判断获取同步状态时调用
- 2 在acquire自旋循环中每次都会获取同步状态时调用
tryRelease:
尝试独占式释放同步状态,返回值为true则表示获取成功,否则获取失败。
调用场景:
- 1 该方法在释放独占式同步状态【release方法】时
6.2 共享式
tryAcquireShared
尝试共享式获取同步状态,当返回值为大于等于0的时获得同步状态成功,否则获取失败。
调用场景:
- 1 在acquireShared开始判断获取同步状态时调用。
- 2 在acquireShared自旋循环中每次都会获取同步状态时调用
tryReleaseShared
尝试共享式释放同步状态,返回值为true则表示获取成功,否则获取失败。
调用场景:
- 该方法在释放共享式同步状态【releaseShared方法】时会调用。
独占式 VS 共享式
**从概念上来说独占式对应只存在一个资源,且只能被一个线程或者说竞争者占用,而共享式对应存在多个资源的是有多个线程或者竞争者能够获取占用。他们对应的场景不同因而流程上会有差异。
在流程上来看只有加粗的部分是共享式所独有的:
尝试获取同步失败 --> 进入等待队列排队 --> 阻塞当前线程 --> 当等待队列排到自己被唤醒 --> 尝试获取锁(可能被其他线程插队而导致获取锁失败,失败在次阻塞,等待下次排到自己)--> 尝试获取锁成功,通知等待队列前面共享节点线程从阻塞中唤醒 --> 执行自己的业务逻辑 --> 尝试释放锁--> 成功通知等待队列前面共享节点线程从阻塞中唤醒。
对于独占式,当节点中线程从阻塞中释放,获取同步状态成功后,就开始执行,直到完成,释放同步状态才会通知等待队列节点线程从阻塞中唤醒,由于独占式通常只有一个资源。因而就没有必要在获取锁成功后通知同步队列线程去尝试获取同步状态,因为自己还没做完呢,也就符合同时只能一人执行特性。只有自己执行完后才通知同步队列线程获取同步状态。
对于共享式,当节点中线程从阻塞中释放,获取同步成功,在开始执行任务前,由于存在多个资源。因而更加积极,他通知后置节点线程从阻塞中唤醒,如果后置节点同样获取同步状态成功,则相当于同时有多个人在执行,也可以说释放锁可以多个线程进入。且这种方式具有传播性。直到某个节点获取同步状态失败才停止这种传播。
无论是独占和共享都提供了模板方法去定制能否获取同步和能否释放同步。
能否获取同步决定了同时又多少人同时执行。和资源相关和独占式还是共享式无关。对于当个资源我们通常使用独占式,对于多个资源我们通常使用共享式。又时候你甚至将获取同步写成默认返回true,表示无锁。就好像是一个闸门绝对能放如多少水进入水库。是不是独占也能实现共享锁呢?答案是可以的。只不过他并没有共享式那么积极!