AQS
AQS是一个多线程编程结构,abstract Queued Synchronizer (笼统行列同步器),可称之为同步器。
在咱们很熟悉的地方用到了这些东西,比如:RenntrantLock 可重入锁 ,ThreadPoolExecutor 线程池。
AQS在源码中被广泛应用,尤其是在JUC(java util Concurrent)中。
回忆
package com.example;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Main main = new Main();
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
main.printLog();
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
main.printLog();
}
});
t1.start();
t2.start();
}
private void printLog() {
try {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "->" + i);
}
} catch (Exception e) {
} finally {
lock.unlock();
}
}
}
可重入锁的最要害部分是: lock.lock()
,便是这个lock办法,
实践上是调用了 sync
目标的lock
办法。sync
目标是一个ReentrantLock
类的一个大局变量。
ReentrantLock
类 有两种完成,公正锁和非公正锁,他们的差异便是是否会依照得到锁的顺序来履行代码
。
Sync
是 该类中的一个笼统静态内部类,它定义了 锁的一些要害行为。
而 上面说的大局sync目标,也分为 公正完成
和非公正完成
,分别是:FairSync
和 NonFairSync
.
非公正锁:
加锁的办法为:经过CAS操作来修正state状况,测验去抢夺锁,假如抢夺成功,就将当时锁的owner设置为 当时线程目标。否则,就履行acquire(1)
去测验获取锁。
acquire办法:
它的逻辑主要是3个:
- tryAcquire 测验获取锁
- addWaiter 假如测验获取锁失利,就将当时线程参加到等候行列
- 假如在测验获取锁失利,而且参加行列成功时,那么 就会选用自旋的办法将线程挂起。
tryAcquire 如下:
也便是说,实践的进程必须由Sync的子类去完成。
在非公正 NonFairSync 中,完成如下:
非公正sync获取 锁的 原理为:
- 判断当时锁的状况,假如是0 无锁状况,就经过CAS(
compareAndSetState
)来判断能否获取锁,能,就直接变更当时lock的owner为当时线程。 - 假如当时是有锁的状况,而且是同一个线程,那么就增加重入次数。
- 上面两种情况都归于取得锁成功,会回来true。
- 上述条件都不满足,便是获取锁失利,回来 false。
RenentrantLock和AQS的联系
能够看到,咱们在使用RentrantLock创建出非公正锁,并履行加锁操作时,大部分逻辑都在AQS结构层完成了。
这种规划形式叫做 模板办法
形式。像是Android中,每一个activity的生命周期函数,咱们去重写的时分,便是在原有的办法基础上参加新逻辑。这个也归于模板办法形式。
他们的联系便是, RenentrantLock
内部定义一个Sync
承继AQS
,并完成自己的要害逻辑,而更底层的线程操控中心逻辑,则在 AQS代码中。这样能够很好地解耦业务代码和结构代码,用户只需求接触到 Lock
,而对加锁解锁的细节无需关心。
AQS原理
state
表明当时锁的状况
- 0 无锁状况
- 非0 则是有锁状况,而且这个数字表明的是同一个线程取得锁的次数,假如一个线程重入5次,那么state便是5,而在开释锁的时分,也要开释5次。直到state为0,其他线程才能取得锁。
它还有一个功能便是完成独占锁
,或许 同享锁
。
独占锁
只有一个线程能够持有,当一个线程去申请锁的时分,先判断state是不是0,假如不是就要等候。
同享锁
答应多个线程持有锁,比如某个操作答应10个线程一起进行,那么超过这个数量的线程就要等候,少于这个数量则能够将state++,然后履行同步语句。
Node
双端行列,而且是等候行列。当多个线程抢夺资源时,会进入此行列。这个行列是 AQS完成多线程同步的中心,
上面的代码中,有两个node,一个头 head,一个尾 tail。 Node源码如下:
默许情况下,一个AQS中头node和尾node都是null。
获取锁失利后流程分析
锁存在的意义便是,使得获取到锁的线程履行同步代码。多个线程抢夺锁,抢夺失利的,就要被堵塞,等候后续的唤醒。
ReentrantLock 让线程等候以及唤醒的操作流程如下:
当调用lock加锁失利时, 会调用 tryAcquire ,addWaiter,acquireQueued ,而且 tryAcquire 在 ReentrantLock被完成。假如tryAcquire回来true,取得锁成功, 才会继续履行同步代码。
下图是 addWaiter源代码:
而当 tryAcquire 回来false时,它会被添加到一个等候行列的结尾。可是刺进纷歧定能成功。
- 当tail为空时,也便是 行列从未被初始化时,此刻需求在行列中刺进一个空的Node
- 当 compareAndSetTail失利时,说明刺进进程中,有线程修正了此行列,因此需求调用enq办法将当时node刺进到行列结尾。
经历过 addWaiter之后,线程以Node形式刺进到了等候行列的结尾。
接下来便是 acquiredQueued办法, 它并不会直接去挂起Node中的线程,在刺进节点的进程中,可能之前持有锁的线程已经开释了锁,此刻,履行自旋去测验获取锁。假如自旋操作仍是没有获取到锁,那么就将该线程挂起或许堵塞。
获取锁流程总结
- AQS模板办法中,acquire 经过调用子类的 tryAcquire来测验获取锁、
- 假如获取失利,则调用 addWaiter将当时线程构造成Node刺进到 同步行列的末尾
- 在 acquireQueued 办法中以自旋的办法测验获取锁
- 假如获取失利,就判断是否需求将当时线程堵塞
- 假如需求堵塞,则调用 native层的lockSupport来完成堵塞
开释锁的流程
开释锁也是从sync目标开端的。
- 先履行tryRelease看看是否能够开释
- 假如能够开释,则调用unparkSuccessor来开释
unparkSuccessor
源码如下:
关于CAS
上面的源代码中屡次说到了 CAS,compareAndSetXXX。 比如说:compareAndSetState。它最终会调用unsafe中的api进行操作。 CAS全称是 CompareAndSwap ,它是经过硬件完成的 并发安全技能。 底层经过CPU的CAS指令,对缓存加锁和总线加锁的办法来完成多处理器之间的原子操作。
完成的具体进程有3个要素:
- 内存值V
- 旧的预期值E
- 要修正的新值U
当且仅当V和E相一起,才将V修正为E,否则什么都不做。
- 假如是多处理器,就经过lock前缀的cmpxchg指令完成缓存桎梏或许总线加锁的办法来完成多处理器之间的原子操作
- 假如是单处理器,直接使用cmpxchg完成原子操作
自定义AQS
本文说到的 ReentrantLock 中的Sync,只是AQS的一种完成,实践上咱们能够经过 承继 AQS的办法相同完成咱们自己的锁同步机制。 比如:
总结
- AQS是一套结构,结构内部定义好了大部分同步所需的逻辑
- 状况指示器state和等候行列 node是AQS的中心
- 由于AQS有两种不同的完成,所以咱们能够用它来完成 独占锁ReentrantLock,或许同享锁 (读写锁)。
假如咱们去重写一套同步机制,需求重写的办法如下: