AQS是什么
AQS是指java.util.concurrent.locks包里的AbstractQueuedSynchronizer类,抽象的FIFO队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架;
其实AQS的想法很简单, 一个直观的对应就是超市的收银台,大家排队买单,获得锁就是排到了,没排到的就是在队列里等待锁。解决竞争最好的办法目前看来就是排队。AQS只是把这个原则用程序实现了而已。
在了解AQS之前先了解下先入先出队列(First Input First Output,FIFO)这是一种传统的按序执行方法,先进入的指令先完成并引退,跟着才执行第二条指令。就是通常情况下的排队。
AQS简单的来说就是使用了一个共享变量来同步状态,该状态由子类去维护,如果被请求的共享资源空闲,则将当前请求线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及唤醒机制,这个机制是用CLH队列来实现的,将暂时获取不到锁的线程加入到队列中
AQS框架做的是
- 线程阻塞队列的维护。
- 线程阻塞和唤醒。
共享变量的操作通过Unsafe类提供的CAS操作完成的。AQS类主要使用的就是2个方法 acquire、release。 - acquire(获得锁):返回true则放线程成继续执行,否则将线程加入等待队列中,等待唤醒.
-
release(释放锁):用于释放锁。释放当前线程队列中的头结点,然后调用唤醒下一个结点的方法。
AQS共享资源
AQS维护了一个volatile int state(共享资源)和一个CLH队列;其中state的访问方式有三种:
- getstate()
- setState()
- compareAndSetState()
AQS定义了两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可以同时执行,如CountDownLatch)
AQS同步器
- isHeldExclusively():线程是否正在独占资源
- tryAcquire():独占方式,尝试获取资源
- tryRelease():独占方式,尝试释放资源
- tryAcquireShared():共享方式,尝试获取资源
- tryReleaseShared():共享方式,尝试释放资源
举一个CoundDownLatch实现过程的例子,任务分为N个子线程去执行,state也初始化为N,每个子线程执行完成之后countDown()一次,state会在CAS操作下减一,当state减为零之后unpark(唤醒)主调用线程
请看下面例子:
package com.concurreny.aqs;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class EasyLock extends AbstractQueuedSynchronizer {
/**
*
*/
private static final long serialVersionUID = 7971529014236952062L;
/**
* 锁定
*/
public void lock(){
acquire(1);
}
/**
* 尝试锁定
*/
public boolean tryLock(){
return tryAcquire(1);
}
/**
* 解锁
*/
public void unlock(){
release(1);
}
/**
* 是否为锁定
*/
public boolean isLocked(){
return isHeldExclusively();
}
/**
* 尝试获取锁
*/
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试释放锁
*/
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
private static final int clientTotal = 30000;
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
final EasyLock easyLock = new EasyLock();
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(30000);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
easyLock.lock();
EasyLock.count++;
easyLock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("统计次数:" + EasyLock.count);
executorService.shutdown();
}
}
可以看出AQS的使用非常简单几乎只需要重写 tryAcquire、tryRelease就可以自己实现一个锁。
读锁与写锁(独占与共享)的实现
CLH队列中的节点的模式是EXCLUSIVE和SHARED模式,当一个线程成功修改了state状态,表示获取了锁,如果线程所在的节点为SHARED模式,将开始一个读锁传递的过程,从头结点,向队列后续节点传递唤醒,直到队列结束或者遇到了EXCLUSIVE模式的节点,等待所有激活的读操作完成,然后进入到EXCLUSIVE模式
CLH队列与Node解析
CLH队列与Node节点
CLH锁即Craig, Landin, and Hagersten (CLH) locks,CLH锁是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。
就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队。
一条队列是队列中每一个人的组织形式。那么每个人决定怎么看待自己在队列中的形态决定了整个队列的形态。比如当每个人都遵守先来后到的原则时,那么最先来的人会站到第一个,之后每个人都会顺序排开。
同样的队列这个类不存在,让他们形成队列的是每个节点类的组织形式。所以想分析队列就必须要先分析节点。
所谓的CLH队列本质上就是一个双向链表Node就是该链表的节点。当然CLH队列并不是简单的双向链表
node节点属性的解析
node节点作为CLH队列的一个节点,有着5条属性,分别是waitStatus 、prev、next、thread、nextWater。下面我们将一一解析这五种属性的作用。
waitStatus介绍
waitStatus是当前节点的一个等待状态标志位,该标志位决定了该节点在当前情况下处于何种状态。
不用再说了,直接看注释吧。这里我们说下Node。Node结点是对每一个访问同步代码的线程的封装,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。
CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
AQS运用该属性时的状态判断
状态 | 判断结果 | 说明 |
---|---|---|
waitStatus=0 | 代表初始化状态 | 该节点尚未被初始化完成 |
waitStatus>0 | 取消状态 | 说明该线程中断或者等待超时,需要移除该线程 |
waitStatus<0 | 有效状态 | 该线程处于可以被唤醒的状态 |
prve next thread介绍
prve 是同步线程队列中保存的前置节点的地址。
next 是同步线程队列中保存的后续节点的地址。
thread 同步线程队列主要存储的线程信息。
nextWaiter介绍
AQS中阻塞队列采用的是用双向链表保存,用prve和next相互链接。而AQS中条件队列是使用单向列表保存的,用
nextWaiter来连接。阻塞队列和条件队列并不是使用的相同的数据结构。
在Node节点的源码中有两个常量属性
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 其他模式
// 其他非空值:条件等待节点(调用Condition的await方法的时候)
nextWaiter实际上标记的就是在该节点唤醒后依据该节点的状态判断是否依据条件唤醒下一个节点。
nextWaiter状态标志 | 说明 |
---|---|
SHARED(共享模式) | 直接唤醒下一个节点 |
EXCLUSIVE(独占模式) | 等待当前线程执行完成后再唤醒 |
其他非空值 | 依据条件决定怎么唤醒下一个线程。类似semaphore中控制几个线程通过 |
node节点的属性介绍完了,下面来介绍node节点的方法以及各个方法的用户
node节点方法解析
构造方法
// 构造方法为空参构造,一般用于创建head节点,或者为nextWaiter设置共享标志。
Node() {
}
// 构造方法用于创建一个带有条件队列的节点
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// 用于创建一个带有初始等waitStatus的节点
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
isShared方法
显而易见这个方法使用来检查当前节点是否为共享节点。
final boolean isShared() {
return nextWaiter == SHARED;
}
predecessor方法
该方法用来查找前置节点是否存在,相当于为前置节点查空。
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
基于Node的的CLH阻塞队列是如何运作的
首先 CLH队列锁通常使用自旋锁来阻塞线程执行,使用本节点和前置节点的waitStatus来判断线程是否阻塞。在前置节点获取执行权限的时候发出信号。每个节点都有一个单独等待通知的监视器,waitStatus不会控制线程是否获取到了锁。获取锁的过程是通过查看队列中的第一个node中的waitStatus是否处于可以执行的状态。如果可执行则继续执行,线程被中断或者超时了就寻找后续node.
CLH锁出列只设置更新头部节点,插入队列只需要原子更新尾部的节点。
首先确定自己是否为头部节点,如果是头部节点则直接获取资源开始执行,如果不是则自旋前置节点直到前置节点执行完成状态修改为CANCELLED,然后断开前置节点的链接,获取资源开始执行。
这部分操作的具体详情会在后续的系列中详细讲解。
总结
CLH阻塞队列采用的是双向链表队列,头部节点默认获取资源获得执行权限。后续节点不断自旋方式查询前置节点是否执行完成,直到头部节点执行完成将自己的waitStatus状态修改以通知后续节点可以获取资源执行。CLH锁是一个有序的无饥饿的公平锁。**