1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

[docs update]并发部分内容修改

This commit is contained in:
guide 2022-07-25 20:46:48 +08:00
parent d45fdb3245
commit 98ec9ae28a
15 changed files with 56 additions and 49 deletions

View File

@ -48,7 +48,7 @@ star: 2
### PDF 面试手册
免费赠送多本优质 PDF 面试手册。
免费赠送多本优质 PDF 面试手册。
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/xingqiu/image-20220723120918434.png)

View File

@ -17,7 +17,7 @@ tag:
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)
![](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 就是一个抽象类,主要用来构建锁和同步器。

View File

@ -75,14 +75,12 @@ public boolean isDone() {
}
```
获取异步计算的结果也非常简单,直接调用 `get()` 方法即可
获取异步计算的结果也非常简单,直接调用 `get()` 方法即可。调用 `get()` 方法的线程会阻塞直到 `CompletableFuture` 完成运算。
```java
rpcResponse = completableFuture.get();
```
注意 `get()` 方法并不会阻塞,因为我们已经知道异步运算的结果了。
如果你已经知道计算的结果的话,可以使用静态方法 `completedFuture()` 来创建 `CompletableFuture`
```java

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -9,7 +9,7 @@ tag:
## 线程池知识回顾
开始这篇文章之前还是简单介绍一嘴线程池,之前写的[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect)这篇文章介绍的很详细了。
开始这篇文章之前还是简单介绍线程池,之前写的[《新手也能看懂的线程池学习总结》](./java-thread-pool-summary.md)这篇文章介绍的很详细了。
### 为什么要使用线程池?
@ -39,7 +39,7 @@ tag:
一般是通过 `ThreadPoolExecutor` 的构造函数来创建线程池,然后提交任务给线程池执行就可以了。
`ThreadPoolExecutor`构造函数如下:
`ThreadPoolExecutor`构造函数如下:
```java
/**
@ -130,11 +130,11 @@ Finished all threads
简单总结一下我了解的使用线程池的时候应该注意的东西,网上似乎还没有专门写这方面的文章。
因为Guide还比较菜有补充和完善的地方可以在评论区告知或者在微信上与我交流。
因为 Guide 还比较菜,有补充和完善的地方,可以在评论区告知或者在微信上与我交流。
### 1. 使用 `ThreadPoolExecutor ` 的构造函数声明线程池
### 1. 使用 `ThreadPoolExecutor` 的构造函数声明线程池
**1. 线程池必须手动通过 `ThreadPoolExecutor ` 的构造函数来声明,避免使用`Executors ` 类的 `newFixedThreadPool``newCachedThreadPool` ,因为可能会有 OOM 的风险。**
**1. 线程池必须手动通过 `ThreadPoolExecutor` 的构造函数来声明,避免使用`Executors` 类的 `newFixedThreadPool``newCachedThreadPool` ,因为可能会有 OOM 的风险。**
> Executors 返回线程池对象的弊端如下:
>
@ -143,7 +143,7 @@ Finished all threads
说白了就是:**使用有界队列,控制线程创建数量。**
除了避免 OOM 的原因之外,不推荐使用 `Executors `提供的两种快捷的线程池的原因还有:
除了避免 OOM 的原因之外,不推荐使用 `Executors`提供的两种快捷的线程池的原因还有:
1. 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
2. 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。
@ -152,7 +152,7 @@ Finished all threads
你可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。
除此之外,我们还可以利用 `ThreadPoolExecutor` 的相关 API做一个简陋的监控。从下图可以看出 `ThreadPoolExecutor`提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。
除此之外,我们还可以利用 `ThreadPoolExecutor` 的相关 API 做一个简陋的监控。从下图可以看出, `ThreadPoolExecutor`提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。
![](./images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png)
@ -263,24 +263,37 @@ public final class NamingThreadFactory implements ThreadFactory {
>
> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
**类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。**
类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。
**如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的 CPU 根本没有得到充分利用。**
**但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。**
- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的CPU 根本没有得到充分利用。
- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
有一个简单并且适用面比较广的公式:
- **CPU 密集型任务(N+1)** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 NCPU 核心数)+1比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断或者其它原因导致的任务暂停而带来的影响。一旦任务暂停CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- **CPU 密集型任务(N+1)** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 NCPU 核心数)+1比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断或者其它原因导致的任务暂停而带来的影响。一旦任务暂停CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- **I/O 密集型任务(2N)** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
**如何判断是 CPU 密集任务还是 IO 密集任务?**
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
> 🌈 拓展一下(参见:[issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)
>
> 线程数更严谨的计算的方法应该是:`最佳线程数 = NCPU 核心数1+WT线程等待时间/ST线程计算时间`,其中 `WT线程等待时间=线程运行总时间 - ST线程计算时间`
>
> 线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。
>
> 我们可以通过 JDK 自带的工具 VisualVM 来查看 `WT/ST` 比例。
>
> CPU 密集型任务的 `WT/ST` 接近或者等于 0因此 线程数可以设置为 NCPU 核心数1+0= N和我们上面说的 NCPU 核心数)+1 差不多。
>
> IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N按道理来说WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。
**公示也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用!**
#### 美团的骚操作
美团技术团队在[《Java线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。
美团技术团队在[《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。
美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:
@ -290,21 +303,18 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
**为什么是这三个参数?**
我在这篇[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect) 中就说过这三个参数是 `ThreadPoolExecutor` 最重要的参数,它们基本决定了线程池对于任务的处理策略。
我在这篇[《新手也能看懂的线程池学习总结》](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485808&idx=1&sn=1013253533d73450cef673aee13267ab&chksm=cea246bbf9d5cfad1c21316340a0ef1609a7457fea4113a1f8d69e8c91e7d9cd6285f5ee1490&token=510053261&lang=zh_CN&scene=21#wechat_redirect) 中就说过这三个参数是 `ThreadPoolExecutor` 最重要的参数,它们基本决定了线程池对于任务的处理策略。
**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。
![](./images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png)
格外需要注意的是`corePoolSize` 程序运行期间的时候,我们调用 `setCorePoolSize `这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。
格外需要注意的是`corePoolSize` 程序运行期间的时候,我们调用 `setCorePoolSize`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的capacity 字段的final关键字修饰给去掉了让它变为可变的
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue` capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
最终实现的可动态修改线程池参数效果如下。👏👏👏
![动态配置线程池参数最终效果](./images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png)
还没看够?推荐 why神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦!
还没看够?推荐 why 神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦!

View File

@ -96,7 +96,7 @@ static final class NonfairSync extends Sync {
带着非公平锁的这些问题,再看下公平锁源码中获锁的方式:
```
```java
// java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync {
@ -218,32 +218,31 @@ private volatile int state;
![](https://p1.meituan.net/travelcube/b8b53a70984668bc68653efe9531573e78636.png)
> 🐛 修正: 图中的一处小错误,(AQS)CAS修改共享资源 State 成功之后应该是获取锁成功(非公平锁)。
> 🐛 修正(参见: [issue#1761](https://github.com/Snailclimb/JavaGuide/issues/1761): 图中的一处小错误,(AQS)CAS修改共享资源 State 成功之后应该是获取锁成功(非公平锁)。
>
> 对应的源码如下:
>
> ```java
> final boolean nonfairTryAcquire(int acquires) {
> final Thread current = Thread.currentThread();//获取当前线程
> int c = getState();
> if (c == 0) {
> if (compareAndSetState(0, acquires)) {//CAS抢锁
> setExclusiveOwnerThread(current);//设置当前线程为独占线程
> return true;//抢锁成功
> }
> }
> else if (current == getExclusiveOwnerThread()) {
> int nextc = c + acquires;
> if (nextc < 0) // overflow
> throw new Error("Maximum lock count exceeded");
> setState(nextc);
> return true;
> }
> return false;
> }
> final Thread current = Thread.currentThread();//获取当前线程
> int c = getState();
> if (c == 0) {
> if (compareAndSetState(0, acquires)) {//CAS抢锁
> setExclusiveOwnerThread(current);//设置当前线程为独占线程
> return true;//抢锁成功
> }
> }
> else if (current == getExclusiveOwnerThread()) {
> int nextc = c + acquires;
> if (nextc < 0) // overflow
> throw new Error("Maximum lock count exceeded");
> setState(nextc);
> return true;
> }
> return false;
> }
> ```
>
>
为了帮助大家理解 ReentrantLock 和 AQS 之间方法的交互过程,以非公平锁为例,我们将加锁和解锁的交互流程单独拎出来强调一下,以便于对后续内容的理解。

View File

@ -170,7 +170,7 @@ new ThreadLocal<>().set(s);
其实是不对的,因为题目说的是在做 `ThreadLocal.get()` 操作,证明其实还是有**强引用**存在的,所以 `key` 并不为 `null`,如下图所示,`ThreadLocal`的**强引用**仍然是存在的。
![image.png](./images/thread-local/5.png)
![](./images/thread-local/5.png)
如果我们的**强引用**不存在的话,那么 `key` 就会被回收,也就是会出现我们 `value` 没被回收,`key` 被回收,导致 `value` 永远存在,出现内存泄漏。
@ -694,7 +694,7 @@ private void resize() {
我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是 4`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。
迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移,此时继续往后迭代,到`index = 6`的时候即找到了`key`值相等的`Entry`数据,如下图所示:
迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移`index 6,7`前移之后,继续从 `index=5` 往后迭代,于是就在 `index=5` 找到了`key`值相等的`Entry`数据,如下图所示:
![](./images/thread-local/28.png)