李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
46.异步模式之工作线程
Leefs
2022-11-21 PM
505℃
0条
[TOC] ### 一、定义 让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。 > 例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那 么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message) 注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率 > 例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。 ### 二、饥饿 > 固定大小线程池会有饥饿现象 **示例** ``` ● 两个工人是同一个线程池中的两个线程 ● 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作 ○ 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待 ○ 后厨做菜:没啥说的,做就是了 ● 比如工人 A 处理了点餐任务,接下来它要等着工人 B 把菜做好,然后上菜,他俩也配合的蛮好 ● 但现在同时来了两个客人,这个时候工人 A 和工人 B 都去处理点餐了,这时没人做饭了,饥饿 ``` **代码示例** ```java import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * Created by lilinchao * Date 2022/11/21 * Description 饥饿状态演示 */ @Slf4j(topic = "c.TestDeadLock") public class TestDeadLock { static final List
MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅"); static Random RANDOM = new Random(); static String cooking() { return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.execute(() -> { //excute执行一个任务,submit又提交一个任务,等于两个任务。一个线程处理点餐,另一个做菜 //两个任务执行不会产生饥饿现象,但是多了就会产生饥饿。 //两个线程都去处理点餐了,没有线程处理做菜了。f.get()无法获取值,陷入死等。但不是死锁。 log.debug("处理点餐..."); Future
f = executorService.submit(() -> { log.debug("做菜"); return cooking(); }); try { log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); /* executorService.execute(() -> { log.debug("处理点餐..."); Future
f = executorService.submit(() -> { log.debug("做菜"); return cooking(); }); try { log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } });*/ } } ``` + **运行结果** ``` 23:11:04.553 [pool-1-thread-1] DEBUG c.TestDeadLock - 处理点餐... 23:11:04.557 [pool-1-thread-2] DEBUG c.TestDeadLock - 做菜 23:11:04.557 [pool-1-thread-1] DEBUG c.TestDeadLock - 上菜: 辣子鸡丁 ``` + **当代码的注释取消后,可能的输出** ``` 23:12:40.310 [pool-1-thread-1] DEBUG c.TestDeadLock - 处理点餐... 23:12:40.310 [pool-1-thread-2] DEBUG c.TestDeadLock - 处理点餐... ``` **结果分析** 上面的代码可以看到, 服务员先处理点餐, 然后新创建线程处理做菜, 当只有一位客人的时候是可以正常处理的, 但是如果有两位客人, 但是线程池大小只有2, 就会使线程处于饥饿状态, 导致死锁。 #### 解决办法 增加线程池的核心线程数也是一种办法, 但是我们不能一直使用这种方法, 这样会减少CPU的利用率, 因为当客人少的时候, 核心线程还是这么多。 > 解决办法也很简单, 不同的任务使用不同的线程池去处理 : > > - 点餐线程使用点餐线程池去处理 > - 做饭线程使用做饭线程池去处理 **代码示例** ```java import lombok.extern.slf4j.Slf4j; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; /** * Created by lilinchao * Date 2022/11/21 * Description 线程饥饿解决办法 */ @Slf4j(topic = "c.TestDeadLock") public class TestDeadLock { static final List
MENU = Arrays.asList("地三鲜", "宫保鸡丁", "辣子鸡丁", "烤鸡翅"); static Random RANDOM = new Random(); static String cooking() { return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { ExecutorService waiterPool = Executors.newFixedThreadPool(1); ExecutorService cookPool = Executors.newFixedThreadPool(1); waiterPool.execute(() -> { log.debug("处理点餐..."); Future
f = cookPool.submit(() -> { log.debug("做菜"); return cooking(); }); try { log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); waiterPool.execute(() -> { log.debug("处理点餐..."); Future
f = cookPool.submit(() -> { log.debug("做菜"); return cooking(); }); try { log.debug("上菜: {}", f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); } } ``` **运行结果** ``` 23:25:01.950 [pool-1-thread-1] DEBUG c.TestDeadLock - 处理点餐... 23:25:01.956 [pool-2-thread-1] DEBUG c.TestDeadLock - 做菜 23:25:01.956 [pool-1-thread-1] DEBUG c.TestDeadLock - 上菜: 烤鸡翅 23:25:01.958 [pool-1-thread-1] DEBUG c.TestDeadLock - 处理点餐... 23:25:01.958 [pool-2-thread-1] DEBUG c.TestDeadLock - 做菜 23:25:01.958 [pool-1-thread-1] DEBUG c.TestDeadLock - 上菜: 辣子鸡丁 ``` **因此,注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。** ### 三、线程池大小设置 **创建多少线程池合适 ?** - 过小会导致程序不能充分地利用系统资源、容易导致饥饿 - 过大会导致更多的线程上下文切换,占用更多内存 通常根据不同类型 : CPU密集型运算 和 IO密集型运算来进行考虑 #### 3.1 CPU 密集型运算 通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费。 #### 3.2 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
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://www.lilinchao.com/archives/2592.html
上一篇
45.ThreadPoolExecutor线程池提交和关闭方法介绍
下一篇
47.任务调度线程池介绍
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
Spring
稀疏数组
Flume
字符串
Sentinel
排序
Http
并发线程
Hadoop
栈
JVM
Nacos
Spark Streaming
二叉树
持有对象
Redis
CentOS
LeetCode刷题
正则表达式
线程池
Tomcat
哈希表
FileBeat
VUE
nginx
RSA加解密
链表
Typora
数据结构
人工智能
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞