并发编程设计之Worker Thread模式:如何避免重复创建线程?

并发编程设计之Worker Thread模式:如何避免重复创建线程?

引言

Thread-Per-Message 模式,对应到现实世界,其实就是委托代办。这种分工模式如果用 Java Thread 实现,频繁地创建、销毁线程非常影响性能,同时无限制地创建线程还可能导致 OOM,所以在 Java 领域使用场景就受限了

要想有效避免线程的频繁创建、销毁以及 OOM 问题,就不得不提今天我们要细聊的,也是 Java 领域使用最多的 Worker Thread 模式。

Worker Thread 模式及其实现

Worker Thread 模式可以类比现实世界里车间的工作模式:车间里的工人,有活儿了,大家一起干,没活儿了就聊聊天等着。Worker Thread 对应到现实世界里,其实指的就是车间里的工人。

你很容易就能想到用阻塞队列做任务池,然后创建固定数量的线程消费阻塞队列中的任务。其实你仔细想会发现,这个方案就是 Java 语言提供的线程池。线程池有很多优点,例如能够避免重复创建、销毁线程,同时能够限制创建线程的上限等等。用 Java 的 Thread 实现 Thread-Per-Message 模式难以应对高并发场景,原因就在于频繁创建、销毁 Java 线程的成本有点高,而且无限制地创建线程还可能导致应用 OOM。线程池,则恰好能解决这些问题

那我们还是以 echo 程序为例,看看如何用线程池来实现。

ExecutorService es = Executors.newFixedThreadPool(500);
final ServerSocketChannel ssc = ServerSocketChannel.open().bind(new InetSocketAddress(8080));
// 处理请求
try {
  while (true) {
  // 接收请求
  SocketChannel sc = ssc.accept();
  // 将请求处理任务提交给线程池
  es.execute(()->{
   try {
     // 读 Socket
     ByteBuffer rb = ByteBuffer.allocateDirect(1024);
     sc.read(rb);
     // 模拟处理请求
     Thread.sleep(2000);
     // 写 Socket
     ByteBuffer wb = (ByteBuffer)rb.flip();
     sc.write(wb);
     // 关闭 Socket
     sc.close();
   }catch(Exception e){
     throw new UncheckedIOException(e);
   }
  });
 }
} finally {
  ssc.close();
  es.shutdown();
}

正确地创建线程池

  • Java 的线程池既能够避免无限制地创建线程导致 OOM,也能避免无限制地接收任务导致
    OOM。强烈建议你用创建有界的队列来接收任务
  • 当请求量大于有界队列的容量时,就需要合理地拒绝请求。如何合理地拒绝呢?这需要你结合具体的业务场景来制定,即便线程池默认的拒绝策略能够满足你的需求,也同样建议你在创建线程池时,清晰地指明拒绝策略
  • 为了便于调试和诊断问题,我也强烈建议你在实际工作中给线程赋予一个业务相关的名字。
ExecutorService es = new ThreadPoolExecutor(50, 500,60L, TimeUnit.SECONDS,
  // 注意要创建有界队列
  new LinkedBlockingQueue<Runnable>(2000),
  // 建议根据业务需求实现 ThreadFactory
  r->{
    return new Thread(r, "echo-"+ r.hashCode());
  },
  // 建议根据业务需求实现 RejectedExecutionHandler
  new ThreadPoolExecutor.CallerRunsPolicy());

避免线程死锁

如果提交到相同线程池的任务不是相互独立的,而是有依赖关系的,那么就有可能导致线程死锁。实际工作中,我就亲历过这种线程死锁的场景。具体现象是应用每运行一段时间偶尔就会处于无响应的状态,监控数据看上去一切都正常,但是实际上已经不能正常工作了

比如下图所示,该应用将一个大型的计算任务分成两个阶段,第一个阶段的任务会等待第二阶段的子任务完成。在这个应用里,每一个阶段都使用了线程池,而且两个阶段使用的还是同一个线程池。
在这里插入图片描述
如果你执行下面的这段代码,会发现它永远执行不到最后一行。执行过程中没有任何异常,但是应用已经停止响应了。

//L1、L2 阶段共用的线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//L1 阶段的闭锁
CountDownLatch l1=new CountDownLatch(2);
for (int i=0; i<2; i++){
  System.out.println("L1");
  // 执行 L1 阶段任务
  es.execute(()->{
   //L2 阶段的闭锁
   CountDownLatch l2=new CountDownLatch(2);
   // 执行 L2 阶段子任务
   for (int j=0; j<2; j++){
     es.execute(()->{
       System.out.println("L2");
       l2.countDown();
     });
   }
   // 等待 L2 阶段任务执行完
   l2.await();
   l1.countDown();
  });
}
// 等着 L1 阶段任务执行完
l1.await();
System.out.println("end");

你会发现线程池中的两个线程全部都阻塞在 l2.await(); 这行代码上了,也就是说,线程池里所有的线程都在等待 L2 阶段的任务执行完,那 L2 阶段的子任务什么时候能够执行完呢?永远都没那一天了,为什么呢?因为线程池里的线程都阻塞了,没有空闲的线程执行 L2 阶段的任务了。

最简单粗暴的办法就是将线程池的最大线程数调大,如果能够确定任务的数量不是非常多的话,这个办法也是可行的,否则这个办法就行不通了。其实这种问题通用的解决方案是为不同的任务创建不同的线程池

总结:
觉得有用的客官可以点赞、关注下!感谢支持🙏谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值