1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-20 22:17:09 +08:00
2019-03-25 17:19:36 +08:00

477 lines
24 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

**目录**
<!-- MarkdownTOC -->
- [1 AQS 简单介绍](#1-aqs-简单介绍)
- [2 AQS 原理](#2-aqs-原理)
- [2.1 AQS 原理概览](#21-aqs-原理概览)
- [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式)
- [2.3 AQS底层使用了模板方法模式](#23-aqs底层使用了模板方法模式)
- [3 Semaphore\(信号量\)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问)
- [4 CountDownLatch 倒计时器](#4-countdownlatch-倒计时器)
- [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法)
- [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例)
- [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足)
- [4.4 CountDownLatch相常见面试题](#44-countdownlatch相常见面试题)
- [5 CyclicBarrier\(循环栅栏\)](#5-cyclicbarrier循环栅栏)
- [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景)
- [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例)
- [5.3 CyclicBarrier和CountDownLatch的区别](#53-cyclicbarrier和countdownlatch的区别)
- [6 ReentrantLock ReentrantReadWriteLock](#6-reentrantlock--reentrantreadwritelock)
<!-- /MarkdownTOC -->
> 常见问题AQS原理;CountDownLatch和CyclicBarrier了解吗,两者的区别是什么用过Semaphore吗
**本节思维导图**
![并发编程面试必备AQS 原理以及 AQS 同步组件总结](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-31/61115865.jpg)
### 1 AQS 简单介绍
AQS的全称为AbstractQueuedSynchronizer这个类在java.util.concurrent.locks包下面
![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS.png)
AQS是一个用来构建锁和同步器的框架使用AQS能简单且高效地构造出应用广泛的大量的同步器比如我们提到的ReentrantLockSemaphore其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的当然我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器
### 2 AQS 原理
> 在面试中被问到并发知识的时候大多都会被问到请你说一下自己对于AQS原理的理解下面给大家一个示例供大家参加面试不是背题大家一定要加入自己的思想即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来
下面大部分内容其实在AQS类注释上已经给出了不过是英语看着比较吃力一点感兴趣的话可以看看源码
#### 2.1 AQS 原理概览
**AQS核心思想是如果被请求的共享资源空闲则将当前请求资源的线程设置为有效的工作线程并且将共享资源设置为锁定状态如果被请求的共享资源被占用那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制这个机制AQS是用CLH队列锁实现的即将暂时获取不到锁的线程加入到队列中**
> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列虚拟的双向队列即不存在队列实例仅存在结点之间的关联关系AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点Node来实现锁的分配
看个AQS(AbstractQueuedSynchronizer)原理图
![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/CLH.png)
AQS使用一个int成员变量来表示同步状态通过内置的FIFO队列来完成获取资源线程的排队工作AQS使用CAS对该同步状态进行原子操作实现对其值的修改
```java
private volatile int state;//共享变量使用volatile修饰保证线程可见性
```
状态信息通过protected类型的getStatesetStatecompareAndSetState进行操作
```java
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地CAS操作将同步状态值设置为给定值update如果当前同步状态的值等于expect期望值
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
```
#### 2.2 AQS 对资源的共享方式
**AQS定义两种资源共享方式**
- **Exclusive**独占只有一个线程能执行如ReentrantLock又可分为公平锁和非公平锁
- 公平锁按照线程在队列中的排队顺序先到者先拿到锁
- 非公平锁当线程要获取锁时无视队列顺序直接去抢锁谁抢到就是谁的
- **Share**共享多个线程可同时执行如Semaphore/CountDownLatchSemaphoreCountDownLatCh CyclicBarrierReadWriteLock 我们都会在后面讲到
ReentrantReadWriteLock 可以看成是组合式因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读
不同的自定义同步器争用共享资源的方式也不同自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可至于具体线程等待队列的维护如获取资源失败入队/唤醒出队等AQS已经在上层已经帮我们实现好了
#### 2.3 AQS底层使用了模板方法模式
同步器的设计是基于模板方法模式的如果需要自定义同步器一般的方式是这样模板方法模式很经典的一个应用
1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法这些重写方法很简单无非是对于共享资源state的获取和释放
2. 将AQS组合在自定义同步组件的实现中并调用其模板方法而这些模板方法会调用使用者重写的方法
这和我们以往通过实现接口的方式有很大区别这是模板方法模式很经典的一个运用下面简单的给大家介绍一下模板方法模式模板方法模式是一个很容易理解的设计模式之一
> 模板方法模式是基于继承主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码举个很简单的例子假如我们要去一个地方的步骤是购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`我们可能乘坐不同的交通工具回家比如飞机或者火车所以除了`ride()`方法其他方法的实现几乎相同我们可以定义一个包含了这些方法的抽象类然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法
**AQS使用了模板方法模式自定义同步器时需要重写下面几个AQS提供的模板方法**
```java
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源成功则返回true失败则返回false。
tryRelease(int)//独占方式。尝试释放资源成功则返回true失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败0表示成功但没有剩余可用资源正数表示成功且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源成功则返回true失败则返回false。
```
默认情况下每个方法都抛出 `UnsupportedOperationException` 这些方法的实现必须是内部线程安全的并且通常应该简短而不是阻塞AQS类中的其他方法都是final 所以无法被其他类使用只有这几个方法可以被其他类使用
以ReentrantLock为例state初始化为0表示未锁定状态A线程lock()会调用tryAcquire()独占该锁并将state+1此后其他线程再tryAcquire()时就会失败直到A线程unlock()到state=0即释放锁为止其它线程才有机会获取该锁当然释放锁之前A线程自己是可以重复获取此锁的state会累加这就是可重入的概念但要注意获取多少次就要释放多么次这样才能保证state是能回到零态的
再以CountDownLatch以例任务分为N个子线程去执行state也初始化为N注意N要与线程个数一致这N个子线程是并行执行的每个子线程执行完后countDown()一次state会CAS(Compare and Swap)减1等到所有子线程都执行完后(即state=0)会unpark()主调用线程然后主调用线程就会从await()函数返回继续后余动作
一般来说自定义同步器要么是独占方法要么是共享方式他们也只需实现`tryAcquire-tryRelease``tryAcquireShared-tryReleaseShared`中的一种即可但AQS也支持自定义同步器同时实现独占和共享两种方式`ReentrantReadWriteLock`
推荐两篇 AQS 原理和相关源码分析的文章
- http://www.cnblogs.com/waterystone/p/4920797.html
- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
### 3 Semaphore(信号量)-允许多个线程同时访问
**synchronized ReentrantLock 都是一次只允许一个线程访问某个资源Semaphore(信号量)可以指定多个线程同时访问某个资源**示例代码如下
```java
/**
*
* @author Snailclimb
* @date 2018年9月30日
* @Description: 需要一次性拿一个许可的情况
*/
public class SemaphoreExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允许执行的线程数量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
semaphore.acquire();// 获取一个许可所以可运行线程数量为20/1=20
test(threadnum);
semaphore.release();// 释放一个许可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
```
执行 `acquire` 方法阻塞直到有一个许可证可以获得然后拿走一个许可证每个 `release` 方法增加一个许可证这可能会释放一个阻塞的acquire方法然而其实并没有实际的许可证这个对象Semaphore只是维持了一个可获得许可证的数量 Semaphore经常用于限制获取某种资源的线程数量
当然一次也可以一次拿取和释放多个许可不过一般没有必要这样做
```java
semaphore.acquire(5);// 获取5个许可所以可运行线程数量为20/5=4
test(threadnum);
semaphore.release(5);// 获取5个许可所以可运行线程数量为20/5=4
```
除了 `acquire`方法之外另一个比较常用的与之对应的方法是`tryAcquire`方法该方法如果获取不到许可就立即返回false
Semaphore 有两种模式公平模式和非公平模式
- **公平模式** 调用acquire的顺序就是获取许可证的顺序遵循FIFO
- **非公平模式** 抢占式的
**Semaphore 对应的两个构造方法如下**
```java
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
```
**这两个构造方法都必须提供许可的数量第二个构造方法可以指定是公平模式还是非公平模式默认非公平模式**
由于篇幅问题如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章
- https://blog.csdn.net/qq_19431333/article/details/70212663
### 4 CountDownLatch 倒计时器
CountDownLatch是一个同步工具类它允许一个或多个线程一直等待直到其他线程的操作执行完后再执行在Java并发中countdownlatch的概念是一个常见的面试题所以一定要确保你很好的理解了它
#### 4.1 CountDownLatch 的三种典型用法
某一线程在开始运行前等待n个线程执行完毕 CountDownLatch 的计数器初始化为n `new CountDownLatch(n) `每当一个任务线程执行完毕就将计数器减1 `countdownlatch.countDown()`当计数器的值变为0时`CountDownLatch上 await()` 的线程就会被唤醒一个典型应用场景就是启动一个服务时主线程需要等待多个组件加载完毕之后再继续执行
实现多个线程开始执行任务的最大并行性注意是并行性不是并发强调的是多个线程在某一时刻同时开始执行类似于赛跑将多个线程放到起点等待发令枪响然后同时开跑做法是初始化一个共享的 `CountDownLatch` 对象将其计数器初始化为 1 `new CountDownLatch(1) `多个线程在开始执行任务前首先 `coundownlatch.await()`当主线程调用 countDown() 计数器变为0多个线程同时被唤醒
死锁检测一个非常方便的使用场景是你可以使用n个线程访问共享资源在每次测试阶段的线程数目是不同的并尝试产生死锁
#### 4.2 CountDownLatch 的使用示例
```java
/**
*
* @author SnailClimb
* @date 2018年10月1日
* @Description: CountDownLatch 使用方法示例
*/
public class CountDownLatchExample1 {
// 请求的数量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表达式的运用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一个请求已经被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模拟请求的耗时操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模拟请求的耗时操作
}
}
```
上面的代码中我们定义了请求的数量为550当这550个请求被处理完成之后才会执行`System.out.println("finish");`
与CountDownLatch的第一次交互是主线程等待其他线程主线程必须在启动其他线程后立即调用CountDownLatch.await()方法这样主线程的操作就会在这个方法上阻塞直到其他线程完成各自的任务
其他N个线程必须引用闭锁对象因为他们需要通知CountDownLatch对象他们已经完成了各自的任务这种通知机制是通过 CountDownLatch.countDown()方法来完成的每调用一次这个方法在构造函数中初始化的count值就减1所以当N个线程都调 用了这个方法count的值等于0然后主线程就能通过await()方法恢复执行自己的任务
#### 4.3 CountDownLatch 的不足
CountDownLatch是一次性的计数器的值只能在构造方法中初始化一次之后没有任何机制再次对其设置值当CountDownLatch使用完毕后它不能再次被使用
#### 4.4 CountDownLatch相常见面试题
解释一下CountDownLatch概念
CountDownLatch 和CyclicBarrier的不同之处
给出一些CountDownLatch使用的例子
CountDownLatch 类中主要的方法
### 5 CyclicBarrier(循环栅栏)
CyclicBarrier CountDownLatch 非常类似它也可以实现线程间的技术等待但是它的功能比 CountDownLatch 更加复杂和强大主要应用场景和 CountDownLatch 类似
CyclicBarrier 的字面意思是可循环使用Cyclic的屏障Barrier它要做的事情是让一组线程到达一个屏障也可以叫同步点时被阻塞直到最后一个线程到达屏障时屏障才会开门所有被屏障拦截的线程才会继续干活CyclicBarrier默认的构造方法是 `CyclicBarrier(int parties)`其参数表示屏障拦截的线程数量每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障然后当前线程被阻塞
#### 5.1 CyclicBarrier 的应用场景
CyclicBarrier 可以用于多线程计算数据最后合并计算结果的应用场景比如我们用一个Excel保存了用户所有银行流水每个Sheet保存一个帐户近一年的每笔银行流水现在需要统计用户的日均银行流水先用多线程处理每个sheet里的银行流水都执行完之后得到每个sheet的日均银行流水最后再用barrierAction用这些线程的计算结果计算出整个Excel的日均银行流水
#### 5.2 CyclicBarrier 的使用示例
示例1
```java
/**
*
* @author Snailclimb
* @date 2018年10月1日
* @Description: 测试 CyclicBarrier 类中带参数的 await() 方法
*/
public class CyclicBarrierExample2 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
```
运行结果如下
```
threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:0is finish
threadnum:1is finish
threadnum:2is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
threadnum:9is finish
threadnum:5is finish
threadnum:8is finish
threadnum:7is finish
threadnum:6is finish
......
```
可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候 `await`方法之后的方法才被执行
另外CyclicBarrier还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`用于在线程到达屏障时优先执行`barrierAction`方便处理更复杂的业务场景示例代码如下
```java
/**
*
* @author SnailClimb
* @date 2018年10月1日
* @Description: 新建 CyclicBarrier 的时候指定一个 Runnable
*/
public class CyclicBarrierExample3 {
// 请求的数量
private static final int threadCount = 550;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("------当线程数达到之后,优先执行------");
});
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
cyclicBarrier.await();
System.out.println("threadnum:" + threadnum + "is finish");
}
}
```
运行结果如下
```
threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
------当线程数达到之后优先执行------
threadnum:4is finish
threadnum:0is finish
threadnum:2is finish
threadnum:1is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
------当线程数达到之后优先执行------
threadnum:9is finish
threadnum:5is finish
threadnum:6is finish
threadnum:8is finish
threadnum:7is finish
......
```
#### 5.3 CyclicBarrier和CountDownLatch的区别
CountDownLatch是计数器只能使用一次而CyclicBarrier的计数器提供reset功能可以多次使用但是我不那么认为它们之间的区别仅仅就是这么简单的一点我们来从jdk作者设计的目的来看javadoc是这么描述它们的
> CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程等待其他多个线程完成某件事情之后才能执行)
> CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待直到到达同一个同步点再继续一起执行)
对于CountDownLatch来说重点是一个线程多个线程等待而其他的N个线程在完成某件事情之后可以终止也可以等待而对于CyclicBarrier重点是多个线程在任意一个线程没有完成所有的线程都必须等待
CountDownLatch是计数器线程完成一个记录一个只不过计数不是递增而是递减而CyclicBarrier更像是一个阀门需要所有线程都到达阀门才能打开然后继续执行
![CyclicBarrier和CountDownLatch的区别](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS333.png)
CyclicBarrier和CountDownLatch的区别这部分内容参考了如下两篇文章
- https://blog.csdn.net/u010185262/article/details/54692886
- https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0
### 6 ReentrantLock ReentrantReadWriteLock
ReentrantLock synchronized 的区别在上面已经讲过了这里就不多做讲解另外需要注意的是读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读所以在读操作远大于写操作的时候读写锁就非常有用了
由于篇幅问题关于 ReentrantLock ReentrantReadWriteLock 详细内容可以查看我的这篇原创文章
- [ReentrantLock ReentrantReadWriteLock](https://mp.weixin.qq.com/s?__biz=MzU4NDQ4MzU5OA==&mid=2247483745&idx=2&sn=6778ee954a19816310df54ef9a3c2f8a&chksm=fd985700caefde16b9970f5e093b0c140d3121fb3a8458b11871e5e9723c5fd1b5a961fd2228&token=1829606453&lang=zh_CN#rd)