异步模式-工作线程

本文介绍了异步模式中工作线程的概念,通过线程池模拟示例解释了饥饿问题及其对性能的影响。讨论了解决饥饿的方法,即根据任务类型划分线程池。最后,探讨了如何根据CPU密集型和I/O密集型任务来合理创建线程池的大小,以平衡资源利用和避免饥饿现象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异步模式-工作线程

1.定义

让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。
例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。

2.饥饿

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

  • 两个工人是同一个线程池中的两个线程
  • 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
    • 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
    • 后厨做菜:没啥说的,做就是了
  • 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
  • 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿

线程不足引起的饥饿代码演示

(1)1个线程点餐,1个线程做菜不会产生饥饿

@Test
    public void test_starvation() {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        //1个线程点餐,1个线程做菜,不会饥饿
        pool.execute(() -> {
            log.debug("开始点餐");
            Future<String> future = pool.submit(() -> {
                log.debug("做菜...");
                return cooking();
            });
            try {
                log.debug("上菜:{}", future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        while (true) ;
    }

-------------输出--------------
20:56:45.524 [pool-1-thread-1] DEBUG c.Test_ThreadPoolStarvation - 开始点餐
20:56:45.564 [pool-1-thread-2] DEBUG c.Test_ThreadPoolStarvation - 做菜...
20:56:45.564 [pool-1-thread-1] DEBUG c.Test_ThreadPoolStarvation - 上菜:地三鲜

(2)当执行两个点餐时,2个线程都跑去点餐,没有线程做饭,产生饥饿

static final List<String> MENU = Arrays.asList("地三鲜", "辣子鸡", "可乐");
    static Random RANDOM = new Random();

    static String cooking() {
        return MENU.get(RANDOM.nextInt(MENU.size()));
    }
    
@Test
    public void test_starvation() {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        //1个线程点餐,1个线程做菜,不会饥饿
        pool.execute(() -> {
            log.debug("开始点餐");
            Future<String> future = pool.submit(() -> {
                log.debug("做菜...");
                return cooking();
            });
            try {
                log.debug("上菜:{}", future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        pool.execute(() -> {
            log.debug("开始点餐");
            Future<String> future = pool.submit(() -> {
                log.debug("做菜...");
                return cooking();
            });
            try {
                log.debug("上菜:{}", future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        while (true) ;
    }

---------------输出----------------
20:59:47.424 [pool-1-thread-2] DEBUG c.Test_ThreadPoolStarvation - 开始点餐
20:59:47.414 [pool-1-thread-1] DEBUG c.Test_ThreadPoolStarvation - 开始点餐

3.饥饿的解决方法

不同的任务类型使用不同的线程池!可以有效避免饥饿的发生

static final List<String> MENU = Arrays.asList("地三鲜", "辣子鸡", "可乐");
static Random RANDOM = new Random();

static String cooking() {
      return MENU.get(RANDOM.nextInt(MENU.size()));
}

    @Test
    public void test_starvationResolving() {
        //2个点餐线程
        ExecutorService waiter = Executors.newFixedThreadPool(2);
        //2个做饭线程
        ExecutorService cooker = Executors.newFixedThreadPool(2);

        waiter.execute(() -> {
            log.debug("开始点餐...");
            Future<String> future = cooker.submit(() -> {
                log.debug("做饭...");
                return cooking();
            });
            try {
                log.debug("上菜:{}", future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        waiter.execute(() -> {
            log.debug("开始点餐...");
            Future<String> future = cooker.submit(() -> {
                log.debug("做饭...");
                return cooking();
            });
            try {
                log.debug("上菜:{}", future.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        while (true) ;
    }

--------------输出-----------------
21:09:44.706 [pool-1-thread-2] DEBUG c.Test_ThreadPoolStarvation - 开始点餐...
21:09:44.698 [pool-1-thread-1] DEBUG c.Test_ThreadPoolStarvation - 开始点餐...
21:09:44.711 [pool-2-thread-2] DEBUG c.Test_ThreadPoolStarvation - 做饭...
21:09:44.712 [pool-1-thread-1] DEBUG c.Test_ThreadPoolStarvation - 上菜:可乐
21:09:44.714 [pool-2-thread-1] DEBUG c.Test_ThreadPoolStarvation - 做饭...
21:09:44.714 [pool-1-thread-2] DEBUG c.Test_ThreadPoolStarvation - 上菜:地三鲜

4.创建多少线程池合适

  • 过小会导致程序不能充分地利用系统资源、容易导致饥饿
  • 过大会导致更多的线程上下文切换,占用更多内存

(1) CPU密集运算

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

(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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值