异步模式之工作线程

简介: 异步模式之工作线程
定义

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现 就是线程池,也体现了经典设计模式中的享元模式。

例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那 么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)

注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成 服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工

饥饿

固定大小线程池会有饥饿现象

两个工人是同一个线程池中的两个线程

他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作

  • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
  • 后厨做菜:没啥说的,做就是了

比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好

但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿

1. public class Test {
2. public static void main(String[] args) {
3. ExecutorService executorService = Executors.newFixedThreadPool(2);
4.         executorService.submit(()->{
5.             System.out.println("点餐中...");
6.             Future<String> future = executorService.submit(() -> {
7. return "宫保鸡丁1";
8.             });
9. try {
10. String s = future.get();
11.                 System.out.println("上菜"+s);
12.             } catch (InterruptedException e) {
13.                 e.printStackTrace();
14.             } catch (ExecutionException e) {
15.                 e.printStackTrace();
16.             }
17.         });
18. 
19.         executorService.submit(()->{
20.             System.out.println("点餐中...");
21.             Future<String> future = executorService.submit(() -> {
22. return "宫保鸡丁2";
23.             });
24. try {
25. String s = future.get();
26.                 System.out.println("上菜"+s);
27.             } catch (InterruptedException e) {
28.                 e.printStackTrace();
29.             } catch (ExecutionException e) {
30.                 e.printStackTrace();
31.             }
32.         });
33. 
34.     }
35. }

运行结果如下:

点餐中...

点餐中...

上菜宫保鸡丁2

上菜宫保鸡丁1

如果修改核心线程为3,即修改代码中

ExecutorService executorService = Executors.newFixedThreadPool(3);

则运行结果如下:

点餐中...

点餐中...

上菜宫保鸡丁2

上菜宫保鸡丁1

解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程池,例如:

1. public class Test {
2. public static void main(String[] args) {
3. ExecutorService orderExecutorService = Executors.newFixedThreadPool(1);
4. ExecutorService cookExecutorService = Executors.newFixedThreadPool(1);
5.         orderExecutorService.submit(()->{
6.             System.out.println("点餐中...");
7.             Future<String> future = cookExecutorService.submit(() -> {
8. return "宫保鸡丁1";
9.             });
10. try {
11. String s = future.get();
12.                 System.out.println("上菜"+s);
13.             } catch (InterruptedException e) {
14.                 e.printStackTrace();
15.             } catch (ExecutionException e) {
16.                 e.printStackTrace();
17.             }
18.         });
19. 
20.         orderExecutorService.submit(()->{
21.             System.out.println("点餐中...");
22.             Future<String> future = cookExecutorService.submit(() -> {
23. return "宫保鸡丁2";
24.             });
25. try {
26. String s = future.get();
27.                 System.out.println("上菜"+s);
28.             } catch (InterruptedException e) {
29.                 e.printStackTrace();
30.             } catch (ExecutionException e) {
31.                 e.printStackTrace();
32.             }
33.         });
34. 
35.     }
36. }

运行结果如下:

点餐中...

上菜宫保鸡丁1

点餐中...

上菜宫保鸡丁2

创建多少线程池合适

线程池的大小应根据具体的应用场景和系统需求来确定。以下是一些建议供参考:

  1. 考虑系统资源:线程池的大小应该与系统可用的资源相匹配。如果将线程池的大小设置得太大,会消耗过多的系统内存和CPU资源,导致系统性能下降;如果将线程池的大小设置得太小,可能无法充分利用系统资源,导致任务排队等待执行。
  2. 考虑任务类型:不同类型的任务对线程池的需求量不同。如果任务是CPU密集型(计算密集型),即任务在执行过程中主要消耗CPU资源,那么线程池的大小可以设置与CPU核心数相等或略大一些;如果任务是IO密集型(输入输出密集型),即任务在执行过程中主要消耗IO操作(如网络请求、文件读写等),那么线程池的大小通常可以设置较大,以便充分利用系统的IO能力。
  3. 考虑任务的响应时间:如果任务对响应时间要求较高,即需要快速响应用户请求,那么可以适当增加线程池的大小,以提高并发能力和响应速度。
  4. 考虑任务排队策略:线程池的大小还应考虑任务排队的策略。如果线程池使用有界队列作为任务缓冲区,当任务数量过多时,超出队列容量的任务将被拒绝执行;如果使用无界队列作为任务缓冲区,则线程池大小可以设置较大,以允许更多的任务排队等待执行。

总之,确定线程池大小需要综合考虑系统资源、任务类型、响应时间和任务排队策略等因素,并进行实际的性能测试和调优。根据实际情况不断调整线程池的大小,以达到最佳的性能和资源利用率。

CPU 密集型运算

通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因 导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

I/O 密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

经验公式如下

线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 50% = 8

例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式

4 * 100% * 100% / 10% = 40


相关文章
|
2月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
108 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
2月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
2月前
|
网络协议 安全 Java
难懂,误点!将多线程技术应用于Python的异步事件循环
难懂,误点!将多线程技术应用于Python的异步事件循环
79 0
|
4月前
|
缓存 Java
异步&线程池 线程池的七大参数 初始化线程的4种方式 【上篇】
这篇文章详细介绍了Java中线程的四种初始化方式,包括继承Thread类、实现Runnable接口、实现Callable接口与FutureTask结合使用,以及使用线程池。同时,还深入探讨了线程池的七大参数及其作用,解释了线程池的运行流程,并列举了四种常见的线程池类型。最后,阐述了在开发中使用线程池的原因,如降低资源消耗、提高响应速度和增强线程的可管理性。
异步&线程池 线程池的七大参数 初始化线程的4种方式 【上篇】
|
3月前
|
设计模式 缓存 Java
谷粒商城笔记+踩坑(14)——异步和线程池
初始化线程的4种方式、线程池详解、异步编排 CompletableFuture
谷粒商城笔记+踩坑(14)——异步和线程池
|
4月前
|
Java 数据库
异步&线程池 CompletableFuture 异步编排 实战应用 【终结篇】
这篇文章通过一个电商商品详情页的实战案例,展示了如何使用`CompletableFuture`进行异步编排,以解决在不同数据库表中查询商品信息的问题,并提供了详细的代码实现和遇到问题(如图片未显示)的解决方案。
异步&线程池 CompletableFuture 异步编排 实战应用 【终结篇】
|
4月前
|
Java
异步&线程池 CompletableFuture 异步编排 【下篇】
这篇文章深入探讨了Java中的`CompletableFuture`类,解释了如何创建异步操作、使用计算完成时的回调方法、异常处理、串行化方法、任务组合以及多任务组合的使用方式,并通过代码示例展示了各种场景下的应用。
异步&线程池 CompletableFuture 异步编排 【下篇】
|
4月前
|
数据采集 Python
多线程和异步
【8月更文挑战第12天】
44 3
|
5月前
|
Java Spring 容器
Spring boot 自定义ThreadPoolTaskExecutor 线程池并进行异步操作
Spring boot 自定义ThreadPoolTaskExecutor 线程池并进行异步操作
252 3
|
4月前
|
Dart API C语言
Dart ffi 使用问题之想在C/C++中创建异步线程来调用Dart方法,如何操作
Dart ffi 使用问题之想在C/C++中创建异步线程来调用Dart方法,如何操作