AQS 指的是 Java中对管程模型的一种抽象实现,和synchronized
一样都是对管程模型的实现。
只不过在我们 Java 中为了补充 synchronized
锁的缺陷,提供了Lock
锁,而 AQS 是对这个锁的一个抽象,将线程的一个竞争,加锁解锁,都和不同的锁子类抽象出来,在抽象类中将加锁和解锁以及阻塞,唤醒,线程排队队列做了一个抽象,即AbstractQueuedSynchronizer
类。
在管程的发展史上,有三种管程模型,分别是Hasen模型、Hoare模型和 MESA模型。现在正在广泛使用的是MESA模型。
目前使用最广泛的是 MESA 管程模型。
管程中引入了条件变量,每个条件变量都有一个等待队列,作用是解决线程之间同步问题。
如:我们synchronized
锁中 wait()
wait()方法还有一个超时参数,为了避免线程进入等待 队列永久阻塞
我们平时通过一定的条件变量,让线程去等待
//条件
while (wirteLockNum > 0) {
lock.wait();
}
唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不 满足了,所以循环检验条件。
lock.notify();
lock.notifyAll();
Java语言的内置管程synchronized
是基于 JVM 实现的,使用 c++语言实现,Java中又叫 Monitor
锁,监视器
java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖 于 ObjectMonitor 实现,这是 JVM 内部基于 C++ 实现的一套机制。
这也就是为什么 Java 中将线程等待,唤醒等方法放在顶级父类中的原因,跟 AQS 和不同的是,synchronized是将等待队列以及同步队列都封装到了 JVM 中。
同理:
C++中一个对象也就是 Java中 Object 对象中,当然是基于 JVM 层面来说,将锁所需要的信息都实现在了 C++ ObjectMonitor对象中
包括同步队列,重入次数,等待队列,以及对象锁的地址,也包括1.5之后优化的一些信息,如线程id,线程地址等。
ObjectMonitor() {
_header = NULL; //对象头 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ; _succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失 败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
执行顺序
在获取锁时,是将当前线程插入到cxq的头部
而释放锁时,默认策略(QMode=0)是:
如果EntryList为空,则将 cxq中的元素按原有顺序插入到EntryList,并唤醒第一个线程,也就是当EntryList为空时,是后来的线程先获取 锁。
_EntryList不为空,直接从_EntryList中唤醒线程。
这里对象头的信息以及1.5之后锁升级的一些细节就不细说了。
AQS 对管程模型实现
定义了一些锁行为接口
加锁,解锁,可打断锁,是否加锁状态,尝试加锁,获取条件队列(用条件变量来控制线程)
AbstractOwnableSynchronizer 抽象出一个接口,拥有锁。
AbstractQueuedSynchronizer 抽象实现类中需要如下几个条件
sys
独占锁)MESA 大致模型需要条件基本满足
但是 AQS抽象队列 为我们考虑的很多,包括是否公平锁,是否独占锁,资源状态为了后续是否可以打断锁等。
JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的 一般是通过一个内部类Sync继承 AQS 将同步器所有调用都映射到Sync对应的方法。
AQS具备特性
以及 AQS 中锁的可用状态
访问以及修改方式
锁访问方式,共享or独占以及 队列中节点转态
不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享 资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出 队等),AQS已经在抽象层实现好了。
自定义同步器实现时主要实现以下几种方法:
AQS 中使用双向链表用于同步队列等待队列,同里也是多个线程竞争会使用 CAS 来操作,确保一个线程更改成功,防止其他线程更改。
AQS 抽象类中定义的双向队列 Node节点
AQS 具体实现锁有很多,这里我们主要说一下 AQS 的流程对 MESA 的流程过程。
说的再多不如上代码演示一遍
private static int sum = 0;
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(()->{
//加锁
lock.lock();
try {
// 临界区代码
// 业务逻辑
for (int j = 0; j < 10000; j++) {
sum++;
}
} finally {
// 解锁
lock.unlock();
}
});
thread.start();
}
Thread.sleep(20000);
System.out.println("sum : " + sum);
}
这里我们来debug跟一遍流程
首先main启动 对所有thread debug
这里有三个线程,都在lock上阻塞
t0 线程开始,此时没有其他线程对state 更改,t0更改成功 然后设置 exclusiveOwnerThread 为t0线程
我们可以看到期望 0 更改 1 肯定是会成功的
成功进来 设置 exclusiveOwnerThread
t0获取锁成功 进来
然后切t1线程
进入获取锁方法acquire
可以看到尝试获取锁,肯定获取失败,t0已经拿到锁了
此时构建双向链表,此时头尾结点是null
所以会走下面构建 Node 节点方法
先构建一个线程是t1的node节点 Node node = new Node(Thread.currentThread(), mode);
尾结点是null 所以肯定是先构建一个空的头 node节点
再次循环将刚才构建的节点,next指向 t1线程 Node 节点,同事t1node的pre指向 头节点
cas设置头尾结点
继续执行 acquireQueued 方法 获取头节点,尝试获取锁
获取失败,当前线程入队完成,需要阻塞,设置前置节点 waitStatus 是-1,用来唤醒之后的节点线程
这里cas设置
成功返回
parkAndCheckInterrupt() 然后阻塞当前线程 ,将t1线程挂起
此时可看到 只剩t0 t2 线程 ,t0线程未释放锁,t1 已经被挂起
t2 还在准备获取锁
继续往下执行 t2
最终也是构建t2 node 节点
这时候尾结点不是空节点而是 t1线程 Node 节点,cas 设置尾结点指向t2线程 Node 节点,t2 pre 指向t1节点也就是当前保存t1节点的尾结点
可以看到
继续调用 enq 入队 继续 acquireQueued 方法 设置前置节点t1 node节点 为 -1
然后 park 阻塞
最后来看解锁 t0释放锁
可以 看到t1被唤醒了
重置中断标识
继续执行
继续循环
获取前置节点以及尝试获取锁
设置 头节点以及将t1 node 节点pre 不指向 头节点 ,头结点next不指向t1 node 节点
t1拿到锁进入 临界区
依次执行t3也是如此
具体流畅图如下:
解锁流程图
从线程竞争到入队以及park unPark 整个流程
多次释放多次减releases 可重入锁解锁,加锁也是在Lock加锁方法里面
可以看到只能自己解自己加的锁,同事解锁后设置 exclusiveOwnerThread 为 Null 。
ReentrantLock 打断方法
非公平锁里打断设置
真正打断方法
如果当前线程拿到锁就打断 同时设置一些属性
这个方法里最终清除链表里的 线程 Node节点
实际上 打断方法传的就是这个值
可以看到对应节点的值
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接: 调用await方法阻塞线程; 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条 件队列)
条件等待队列
AQS中条件队列是使用单向列表保存的,用nextWaiter来连接: 调用await方法阻塞线程; 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条 件队列)
调用Condition#await方法会释放当前持有的锁,然后阻塞当前线程,同时向 Condition队列尾部添加一个节点,所以调用Condition#await方法的时候必须持有锁。
调用Condition#signal方法会将Condition队列的首节点移动到阻塞队列尾部,然后唤 醒因调用Condition#await方法而阻塞的线程(唤醒之后这个线程就可以去竞争锁了),所 以调用Condition#signal方法的时候必须持有锁,持有锁的线程唤醒被因调用 Condition#await方法而阻塞的线程。
通知
Node p = enq(node); 入队 同步队列
放入条件队列里,同时 park 阻塞
条件变量waitStatus是 Node.CONDITION
AQS 里指的我们学习一个东西,并发情况下,构建链表,以及链表节点入队操作。
入队
Java 多线程基础
ReentrantLock用法详解
深入理解信号量Semaphore
深入理解并发三大特性
并发编程之深入理解CAS
深入理解CountDownLatch
Java 线程池