``` 本文已参与「新人创作礼」活动,一起开启掘金创作之路。
## 1. 定义
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。
例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)\
注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率\
例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工
## 2. 饥饿
固定大小线程池会有饥饿现象\
● 两个工人是同一个线程池中的两个线程\
● 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作\
○ 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待\
○ 后厨做菜:没啥说的,做就是了\
● 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好\
● 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,**饥饿**
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();
}
});/
}
}

输出
> 17:21:27.883 c.TestDeadLock [pool-1-thread-1] - 处理点餐...\
> 17:21:27.891 c.TestDeadLock [pool-1-thread-2] - 做菜\
> 17:21:27.891 c.TestDeadLock [pool-1-thread-1] - 上菜: 烤鸡翅
当注释取消后,可能的输出
> 17:08:41.339 c.TestDeadLock [pool-1-thread-2] - 处理点餐...\
> 17:08:41.339 c.TestDeadLock [pool-1-thread-1] - 处理点餐...
解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,不同的任务类型,采用不同的线程池,**例如:**
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();
}
});
}
}

输出
> 17:25:14.626 c.TestDeadLock [pool-1-thread-1] - 处理点餐...\
> 17:25:14.630 c.TestDeadLock [pool-2-thread-1] - 做菜\
> 17:25:14.631 c.TestDeadLock [pool-1-thread-1] - 上菜: 地三鲜\
> 17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 处理点餐...\
> 17:25:14.632 c.TestDeadLock [pool-2-thread-1] - 做菜\
> 17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 上菜: 辣子鸡丁
**因此,注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。**
## **3. 创建多少线程池合适**
- 过小会导致程序不能充分地利用系统资源、容易导致饥饿
- 过大会导致更多的线程上下文切换,占用更多内存
### 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*
### 4. 自定义线程池
[详情见此篇文章https://blog.csdn.net/Mrrr_Li/article/details/121572343https://blog.csdn.net/Mrrr_Li/article/details/121572343](https://blog.csdn.net/Mrrr_Li/article/details/121572343 "详情见此篇文章https://blog.csdn.net/Mrrr_Li/article/details/121572343")