Skip to content

Commit 5a22a83

Browse files
committed
[docs update]完善 Java 线程池最佳实践
1 parent 3cf54eb commit 5a22a83

10 files changed

+84
-19
lines changed

docs/java/basis/why-there-only-value-passing-in-java.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,11 @@ invoke after: 11
195195
## 为什么 Java 不引入引用传递呢?
196196
197197
引用传递看似很好,能在方法内就直接把实参的值修改了,但是,为什么 Java 不引入引用传递呢?
198+
198199
**注意:以下为个人观点看法,并非来自于 Java 官方:**
200+
199201
1. 出于安全考虑,方法内部对值进行的操作,对于调用者都是未知的(把方法定义为接口,调用方不关心具体实现)。你也想象一下,如果拿着银行卡去取钱,取的是 100,扣的是 200,是不是很可怕。
200-
2. Java 之父 James Gosling 在设计之处就看到了 C、C++ 的许多弊端,所以才想着去设计一门新的语言 Java。在他设计 Java 的时候就遵循了简单易用的原则,摒弃了许多开发者一不留意就会造成问题的“特性”,语言本身的东西少了,开发者要学习的东西也少了。
202+
2. Java 之父 James Gosling 在设计之初就看到了 C、C++ 的许多弊端,所以才想着去设计一门新的语言 Java。在他设计 Java 的时候就遵循了简单易用的原则,摒弃了许多开发者一不留意就会造成问题的“特性”,语言本身的东西少了,开发者要学习的东西也少了。
201203
202204
## 总结
203205
Binary file not shown.
Binary file not shown.
Binary file not shown.

docs/java/concurrent/images/thread-pool/线程池使用不当导致死锁.drawio

Lines changed: 0 additions & 1 deletion
This file was deleted.
Binary file not shown.

docs/java/concurrent/java-concurrent-questions-03.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ static class Entry extends WeakReference<ThreadLocal<?>> {
234234

235235
- **`FixedThreadPool``SingleThreadExecutor`** : 使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
236236
- **`CachedThreadPool`** :使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。
237-
- **`ScheduledThreadPool``SingleThreadScheduledExecutor` ** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
237+
- **`ScheduledThreadPool``SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
238238

239239
```java
240240
// 无界队列 LinkedBlockingQueue
@@ -451,15 +451,15 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
451451

452452
**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。
453453

454-
![](./images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png)
454+
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/threadpoolexecutor-methods.png)
455455

456456
格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。
457457

458458
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
459459

460460
最终实现的可动态修改线程池参数效果如下。👏👏👏
461461

462-
![动态配置线程池参数最终效果](./images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png)
462+
![动态配置线程池参数最终效果](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/meituan-dynamically-configuring-thread-pool-parameters.png)
463463

464464
还没看够?推荐 why 神的[如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦!
465465

docs/java/concurrent/java-thread-pool-best-practices.md

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ tag:
2121

2222
除了避免 OOM 的原因之外,不推荐使用 `Executors`提供的两种快捷的线程池的原因还有:
2323

24-
1. 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
25-
2. 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。
24+
- 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
25+
- 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。
2626

2727
## 2、监测线程池运行状态
2828

2929
你可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。
3030

3131
除此之外,我们还可以利用 `ThreadPoolExecutor` 的相关 API 做一个简陋的监控。从下图可以看出, `ThreadPoolExecutor`提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。
3232

33-
![](./images/thread-pool/ddf22709-bff5-45b4-acb7-a3f2e6798608.png)
33+
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/threadpool-methods-information.png)
3434

3535
下面是一个简单的 Demo。`printThreadPoolStatus()`会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。
3636

@@ -59,15 +59,15 @@ public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
5959

6060
一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。
6161

62-
**我们再来看一个真实的事故案例!** (本案例来源自:[《线程池运用不当的一次线上事故》](https://club.perfma.com/article/646639) ,很精彩的一个案例)
62+
**我们再来看一个真实的事故案例!** (本案例来源自:[《线程池运用不当的一次线上事故》](https://club.perfma.com/article/646639) ,很精彩的一个案例)
6363

64-
![案例代码概览](./images/thread-pool/5b9b814d-722a-4116-b066-43dc80fc1dc4.png)
64+
![案例代码概览](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/production-accident-threadpool-sharing-example.png)
6565

6666
上面的代码可能会存在死锁的情况,为什么呢?画个图给大家捋一捋。
6767

68-
试想这样一种极端情况:假如我们线程池的核心线程数为 **n**,父任务(扣费任务)数量为 **n**,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 **"死锁"**
68+
试想这样一种极端情况:假如我们线程池的核心线程数为 **n**,父任务(扣费任务)数量为 **n**,父任务下面有两个子任务(扣费任务下的子任务),其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 **"死锁"**
6969

70-
![线程池使用不当导致死锁](./images/thread-pool/线程池使用不当导致死锁.png)
70+
![线程池使用不当导致死锁](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/production-accident-threadpool-sharing-deadlock.png)
7171

7272
解决方法也很简单,就是新增加一个用于执行子任务的线程池专门为其服务。
7373

@@ -79,7 +79,7 @@ public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
7979

8080
给线程池里的线程命名通常有下面两种方式:
8181

82-
**1、利用 guava 的 `ThreadFactoryBuilder` **
82+
**1、利用 guava 的 `ThreadFactoryBuilder`**
8383

8484
```java
8585
ThreadFactory threadFactory = new ThreadFactoryBuilder()
@@ -183,19 +183,85 @@ CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内
183183

184184
**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。
185185

186-
![](./images/thread-pool/b6fd95a7-4c9d-4fc6-ad26-890adb3f6c4c.png)
186+
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/threadpoolexecutor-methods.png)
187187

188188
格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。
189189

190190
另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
191191

192192
最终实现的可动态修改线程池参数效果如下。👏👏👏
193193

194-
![动态配置线程池参数最终效果](./images/thread-pool/19a0255a-6ef3-4835-98d1-a839d1983332.png)
194+
![动态配置线程池参数最终效果](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/meituan-dynamically-configuring-thread-pool-parameters.png)
195195

196196
还没看够?推荐 why 神的[《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A)这篇文章,深度剖析,很不错哦!
197197

198198
如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目:
199199

200200
- **[Hippo-4](https://github.com/opengoofy/hippo4j)** :一款强大的动态线程池框架,解决了传统线程池使用存在的一些痛点比如线程池参数没办法动态修改、不支持运行时变量的传递、无法执行优雅关闭。除了支持动态修改线程池参数、线程池任务传递上下文,还支持通知报警、运行监控等开箱即用的功能。
201-
- **[Dynamic TP](https://github.com/dromara/dynamic-tp)** :轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持Nacos、Apollo,Zookeeper、Consul、Etcd,可通过SPI自定义实现)。
201+
- **[Dynamic TP](https://github.com/dromara/dynamic-tp)** :轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。
202+
203+
## 6、线程池使用的一些小坑
204+
205+
### 重复创建线程池的坑
206+
207+
线程池是可以复用的,一定不要频繁创建线程池比如一个用户请求到了就单独创建一个线程池。
208+
209+
```java
210+
@GetMapping("wrong")
211+
public String wrong() throws InterruptedException {
212+
// 自定义线程池
213+
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy());
214+
215+
// 处理任务
216+
executor.execute(() -> {
217+
// ......
218+
}
219+
return "OK";
220+
}
221+
```
222+
223+
出现这种问题的原因还是对于线程池认识不够,需要加强线程池的基础知识。
224+
225+
### Spring 内部线程池的坑
226+
227+
使用 Spring 内部线程池时,一定要手动自定义线程池,配置合理的参数,不然会出现生产问题(一个请求创建一个线程)。
228+
229+
```java
230+
@Configuration
231+
@EnableAsync
232+
public class ThreadPoolExecutorConfig {
233+
234+
@Bean(name="threadPoolExecutor")
235+
public Executor threadPoolExecutor(){
236+
ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
237+
int processNum = Runtime.getRuntime().availableProcessors(); // 返回可用处理器的Java虚拟机的数量
238+
int corePoolSize = (int) (processNum / (1 - 0.2));
239+
int maxPoolSize = (int) (processNum / (1 - 0.5));
240+
threadPoolExecutor.setCorePoolSize(corePoolSize); // 核心池大小
241+
threadPoolExecutor.setMaxPoolSize(maxPoolSize); // 最大线程数
242+
threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 队列程度
243+
threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY);
244+
threadPoolExecutor.setDaemon(false);
245+
threadPoolExecutor.setKeepAliveSeconds(300);// 线程空闲时间
246+
threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // 线程名字前缀
247+
return threadPoolExecutor;
248+
}
249+
}
250+
```
251+
252+
### 线程池和 ThreadLocal 共用的坑
253+
254+
线程池和 `ThreadLocal`共用,可能会导致线程从`ThreadLocal`获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 `ThreadLocal` 变量也会被重用,这就导致一个线程可能获取到其他线程的`ThreadLocal` 值。
255+
256+
不要以为代码中没有显示使用线程池就不存在线程池了,像常用的 Web 服务器 Tomcat 处理任务为了提高并发量,就使用到了线程池,并且使用的是基于原生 Java 线程池改进完善得到的自定义线程池。
257+
258+
当然了,你可以将 Tomcat 设置为单线程处理任务。不过,这并不合适,会严重影响其处理任务的速度。
259+
260+
```properties
261+
server.tomcat.max-threads=1
262+
```
263+
264+
解决上述问题比较建议的办法是使用阿里巴巴开源的 `TransmittableThreadLocal`(`TTL`)。`TransmittableThreadLocal`类继承并加强了 JDK 内置的`InheritableThreadLocal`类,在使用线程池等会池化复用线程的执行组件情况下,提供`ThreadLocal`值的传递功能,解决异步执行时上下文传递的问题。
265+
266+
`InheritableThreadLocal` 项目地址: https://github.com/alibaba/transmittable-thread-local 。
267+

docs/java/concurrent/java-thread-pool-summary.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,15 +171,13 @@ Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecu
171171

172172
![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/java/concurrent/executors-inner-threadpool.png)
173173

174-
175-
176174
《阿里巴巴 Java 开发手册》强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
177175

178176
`Executors` 返回线程池对象的弊端如下(后文会详细介绍到):
179177

180178
- **`FixedThreadPool``SingleThreadExecutor`** : 使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
181179
- **`CachedThreadPool`** :使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。
182-
- **`ScheduledThreadPool``SingleThreadScheduledExecutor` ** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
180+
- **`ScheduledThreadPool``SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
183181

184182
```java
185183
// 无界队列 LinkedBlockingQueue

0 commit comments

Comments
 (0)