1.AQS简单介绍
AQS 的全称为(AbstractQueuedSynchronizer),是在 java.util.concurrent.locks 包下面的一个抽象类。它是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量便是同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,以期望它能够成为实现大部分同步需求的基础.使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask(jdk1.7) 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器.
2.AQS原理
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中.
2.1ReentrantLock和AQS的关系
我们来拿ReentrantLock来举例
ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象,这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件.
2.2ReentrantLock的加锁过程
AQS对象内部有一个核心的变量叫做state,是int类型的,代表了加锁的状态。初始状态下,这个state的值是0。另外,这个AQS内部还有一个关键变量,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。
接着线程跑过来调用ReentrantLock的lock()方法尝试进行加锁,这个加锁的过程,直接就是用CAS操作将state值从0变为1,加锁成功后,加锁线程编程自己
ReentrantLock内核中的锁机制实现都是依赖AQS组件的.我们知道ReentrantLock是可重入锁,所以在线程重入的时候,它发现这个加锁线程是自己之后,只需要state的值加1就行,state的值代表重入次数.在递归运算的时候可能会用到这一点.
2.3ReentrantLock的解锁过程和锁的互斥
第二个线程先判断state的值是否为0,如果失败,说明有人加锁了!然后接着线程2会看一下,是不是自己加的锁,如果不是那么加锁失败!
接着,线程2会将自己放入AQS中的一个等待队列,因为自己尝试加锁失败了,此时就要将自己放入队列中来等待,等待线程1释放锁之后,自己就可以重新尝试加锁了
接着,线程1在执行完自己的业务逻辑代码之后,就会释放锁!他释放锁的过程非常的简单,就是将AQS内的state变量的值递减1,如果state值为0,则彻底释放锁,会将“加锁线程”变量也设置为null!
接下来,会从等待队列的队头唤醒线程2重新尝试加锁。 好!线程2现在就重新尝试加锁,这时还是用CAS操作将state从0变为1,此时就会成功,成功之后代表加锁成功,就会将state设置为1。 此外,还要把“加锁线程”设置为线程2自己,同时线程2自己就从等待队列中出队了。
上面的队列是CLH(Craig,Landin,and Hagersten)队列,该队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配.
2.4AQS 对资源的共享方式
独占:只有一个线程能运行,如 ReentrantLock
共享:多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier
3.AQS的示例
3.1Semaphore(信号量)-允许多个线程同时访问
public class SemaphoreDemo01 {
public static void main(String[] args) {
//设置三个车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
//获得一个车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到了车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "溜了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放车位
semaphore.release();
}
}).start();
}
}
}
- semaphore.acquire();获得,假设已经满了,等待被释放为止
- semaphore.release();释放,会将当前的信号量释放+1,然后唤醒释放的线程
- 作用:多个共享资源互斥!并发限流,控制最大的线程数!
3.2CyclicBarrier-同步屏障
public class CycliBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
System.out.println("神龙被召唤出来啦");
});
for (int i = 1; i <= 7; i++) {
final int temp = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "收集了" + temp + "星珠");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "的" + temp + "星珠变成了石头");
}).start();
}
}
}
在没有达到第7个屏障之前,所有的线程都会阻塞
3.3CountDownLatch(倒计时器)
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
//出去一只小鸡崽子
System.out.println(Thread.currentThread().getName() + "go out");
//数量减1
countDownLatch.countDown();
}).start();
}
try {
//等待计算器归0,否则一直阻塞
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//一定在最后执行
System.out.println("Close Door");
}
}
3.4CyclicBarrier 和 CountDownLatch 的区别
- 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。拿上述例子来说在所有的龙珠没达到七颗之前龙珠是不会变成石头的,所以线程还是被阻塞的,但是对于CountDownLatch,小鸡崽子可以直接出去,然后线程结束
- CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。龙珠必须要七颗在一起才能召唤神龙,小鸡崽子出去了就行,不管你们在不在一起!
参考:
https://snailclimb.gitee.io/javaguide/#/docs/java/multi-thread
https://zhuanlan.zhihu.com/p/86072774