Day853.WorkerThread模式 -Java 性能调优实战

news2024/12/23 19:04:00

WorkerThread模式

Hi,我是阿昌,今天学习记录的是关于WorkerThread模式的内容。

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

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


一、Worker Thread 模式及其实现

Worker Thread 模式可以类比现实世界里车间的工作模式:

车间里的工人,有活儿了,大家一起干,没活儿了就聊聊天等着。

可以参考下面的示意图来理解,Worker Thread 模式中 Worker Thread 对应到现实世界里,其实指的就是车间里的工人。

需要注意的是,车间里的工人数量往往是确定的。

在这里插入图片描述
那在编程领域该如何模拟车间的这种工作模式呢?

或者说如何去实现 Worker Thread 模式呢?

通过上面的图,很容易就能想到用阻塞队列做任务池,然后创建固定数量的线程消费阻塞队列中的任务。这个方案就是 Java 语言提供的线程池

线程池有很多优点,例如能够避免重复创建、销毁线程,同时能够限制创建线程的上限等等。

用 Java 的 Thread 实现 Thread-Per-Message 模式难以应对高并发场景,原因就在于频繁创建、销毁 Java 线程的成本有点高,而且无限制地创建线程还可能导致应用 OOM。

线程池,则恰好能解决这些问题。那我们还是以 echo 程序为例,看看如何用线程池来实现。

下面的示例代码是用线程池实现的 echo 服务端,相比于 Thread-Per-Message 模式的实现,改动非常少,仅仅是创建了一个最多线程数为 500 的线程池 es,然后通过 es.execute() 方法将请求处理的任务提交给线程池处理。

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。

只不过后者经常容易被我们忽略,例如在上面的实现中,就被我们忽略了。所以强烈建议你用创建有界的队列来接收任务。

当请求量大于有界队列的容量时,就需要合理地拒绝请求。如何合理地拒绝呢?

这需要结合具体的业务场景来制定,即便线程池默认的拒绝策略能够满足需求,也同样建议在创建线程池时,清晰地指明拒绝策略。

同时,为了便于调试和诊断问题,也强烈建议在实际工作中给线程赋予一个业务相关的名字。

综合以上这三点建议,echo 程序中创建线程可以使用下面的示例代码。


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 阶段的任务了。
在这里插入图片描述
原因找到了,那如何解决就简单了,最简单粗暴的办法就是将线程池的最大线程数调大,如果能够确定任务的数量不是非常多的话,这个办法也是可行的,否则这个办法就行不通了。其实这种问题通用的解决方案是为不同的任务创建不同的线程池。

对于上面的这个应用,L1 阶段的任务和 L2 阶段的任务如果各自都有自己的线程池,就不会出现这种问题了。

最后再次强调一下:提交到相同线程池中的任务一定是相互独立的,否则就一定要慎重。


四、总结

解决并发编程里的分工问题,最好的办法是和现实世界做对比。

对比现实世界构建编程领域的模型,能够让模型更容易理解。

Thread-Per-Message 模式,类似于现实世界里的委托他人办理,而今天介绍的 Worker Thread 模式则类似于车间里工人的工作模式。如果你在设计阶段,发现对业务模型建模之后,模型非常类似于车间的工作模式,那基本上就能确定可以在实现阶段采用 Worker Thread 模式来实现。


Worker Thread 模式和 Thread-Per-Message 模式的区别有哪些呢?

从现实世界的角度看,你委托代办人做事,往往是和代办人直接沟通的;对应到编程领域,其实现也是主线程直接创建了一个子线程,主子线程之间是可以直接通信的。

而车间工人的工作方式则是完全围绕任务展开的,一个具体的任务被哪个工人执行,预先是无法知道的;

对应到编程领域,则是主线程提交任务到线程池,但主线程并不关心任务被哪个线程执行。

Worker Thread 模式能避免线程频繁创建、销毁的问题,而且能够限制线程的最大数量。Java 语言里可以直接使用线程池来实现 Worker Thread 模式,线程池是一个非常基础和优秀的工具类,甚至有些大厂的编码规范都不允许用 new Thread() 来创建线程的,必须使用线程池。

不过使用线程池还是需要格外谨慎的,除了今天重点讲到的如何正确创建线程池、如何避免线程死锁问题,还需要注意前面我们曾经提到的 ThreadLocal 内存泄露问题。

同时对于提交到线程池的任务,还要做好异常处理,避免异常的任务从眼前溜走,从业务的角度看,有时没有发现异常的任务后果往往都很严重。


阿昌同学写了如下的代码,本义是异步地打印字符串“QQ”,请问他的实现是否有问题呢?


ExecutorService pool = Executors
  .newSingleThreadExecutor();
pool.submit(() -> {
  try {
    String qq=pool.submit(()->"QQ").get();
    System.out.println(qq);
  } catch (Exception e) {
  }
});

结论是:代码会被一直阻塞;

原因是:

  1. 通过Executors.newSingleThreadExecutor()创建的线程池默认是1个核心线程 + 无界工作队列;

  2. 第一次submit时,会把池中唯一的一个核心线程给占用;

  3. 第二次submit时,由于没有空闲的线程,并且工作队列也没满,所以线程池会把提交的任务添加到工作队列,然后等待空闲线程来执行该任务;

  4. 在第二次submit时使用了.get()方法,这里会一直等到线程返回执行结果;

  5. 由于两次submit是嵌套执行的,并且此时线程池中也没有空闲线程,所以第二次submit的任务永远不会被执行,.get()方法会就被永远阻塞,从而导致第一次submit的线程也被永远阻塞。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/148665.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

场景编程集锦 - 吉米的总统梦想

1. 场景描述 吉米是太平洋岛国一个贫苦家庭的孩子&#xff0c;他的梦想就是当总统&#xff0c;引领国家走向富强之路。 开学的第一堂课上&#xff0c;老师用白色的粉笔在黑板上写下了“我的梦想”&#xff0c;同学们都陷入了思考。大卫的梦想是当一名科学家&#xff0c;用奇思妙…

CSS初级教程(文本)【第六天】

文章目录【1】CSS 文本[字体颜色|背景色]【2】CSS 文本对齐【3】CSS 文字装饰【4】CSS 文本转换[大写或小写]【5】CSS 文字间距【6】CSS 文本阴影【7】所有 CSS 文本属性CSS上回学习链接 CSS初级教程 颜色【第一天】 CSS初级教程 背景【第二天】 CSS初级教程 边框【第三天】 CS…

Windows访问控制 -- SID

Windows访问控制是一个比较大的题目&#xff0c;因此计划用一系列的文章简单谈一下这个。本篇是开篇&#xff0c;介绍SID。 Windows访问控制定义 Windows访问控制的含义可以参考MSDN的描述&#xff1a;Access control refers to security features that control who can acce…

Java集合容器介绍

Java 容器分为 Collection 和 Map 两大类&#xff0c;其下又有很多子类&#xff0c;如下所示&#xff1a; Collection接口&#xff1a;单列数据&#xff0c;定义了存取一组对象的方法的集合 1、List&#xff1a;元素有序(指的是存储时&#xff0c;与存放顺序保持一致)、可重复的…

【Docker】(四)使用volume持久化Docker容器中的Redis数据

1.前言 本系列文章记录了从0开始学习Docker的过程&#xff0c;Docker系列历史文章&#xff1a; &#xff08;一&#xff09;基本概念与安装使用 &#xff08;二&#xff09;如何使用Docker发布一个SpringBoot服务 &#xff08;三&#xff09;使用registry远程镜像仓库管理镜像…

[ 数据结构 ] 赫夫曼编码--------数据、文件压缩解压

0 引出 如上图:给定字符串按定长编码处理,最终对应二进制长度为359 思考:如何压缩,将359有效降低? ----回顾:赫夫曼树 1 数据压缩 拿到数据(字符串)的第一反应,虽然知道应该也像上面一样转为字节数组,但就不知道该怎么办了?统计数组中各字节使用的次数,将次数作为权值,字节…

2023.1.8 学习周报

文章目录摘要文献阅读1.题目2.摘要3.介绍4.论文主要贡献5.相关工作5.1 序列感知的推荐系统5.2 神经注意模型6.模型&#xff1a;ATTREC6.1 序列推荐6.2 基于Self-Attention的用户短期兴趣建模6.3 用户长期兴趣建模6.4 模型学习7.实验7.1 数据集7.2 评估指标7.3 模型比较7.4 实验…

SSO单点登录实例详解(前端传Code授权登录)

什么是 SSO&#xff08;单点登录&#xff09; SSO 英文全称 Single Sign On&#xff0c;单点登录。SSO 是在多个应用系统中&#xff0c;用户只需要登录一次就可以访问所有相互信任的应用系统。 单点登录流程 单点登录大致流程如下所示&#xff1a; 单点登录详细流程&#x…

【自学C++】C++变量初始化

C变量初始化 C变量初始化教程 变量 的初始化就是在定义变量的同时&#xff0c;给变量设置一个初始值&#xff0c;在 C 中&#xff0c;如果定义变量没有初始化&#xff0c;那么变量有可能会被赋值也有可能不会赋值。 如果是定义的 全局变量 或者 静态变量&#xff0c;未初始化…

2022年语音合成(TTS)和语音识别(ASR)年度总结

论文统计每月更新一次&#xff0c;主要跟踪语音合成和语音识别的发展状况(很多文章都是在会议后才发出&#xff0c;但不影响统计。统计过程难免存在疏漏&#xff0c;因此统计结果仅供参考。所有文章语音合成领域统计列表请访问http://yqli.tech/page/tts_paper.html&#xff0c…

绝大多数人远远低估了软件开发的难度

给你付钱了&#xff0c;你应该把软件做好&#xff01; 这个话相当于&#xff1a; 给你付钱了&#xff0c;你应该把月亮摘下来&#xff01; 趣讲大白话&#xff1a;臣妾做不到 ********** 软件是特殊商品服务 可以说很难有标准 开发的难度取决于需求多少&#xff0c;技术难度&a…

Java Map集合的介绍和使用

什么是Map类型的集合 介绍 1.用于保存具有映射关系的数据&#xff08;key——value&#xff09;。 2.Map中的key和value可以是任意的类型的数据。 3.Map中的key值不允许重复。 4.Map中的value值可以重复。 5.一般常用string作为value的key。 6.key和value之间存在一一对…

如何进行地图SDK开发(二)——示例文档

概述 前面的文章文章我们写到了SDK的开发以及ak认证的实现&#xff0c;在本文我们继续讲讲地图SDK开发中的示例文档的实现。 技术点 vue3viteelement-plusmonaco-editor 实现后效果 实现 1. 工程初始化 1.1 搭建工程 搭建工程的过程请参照博文(使用vite搭建vue3项目&…

javaEE初阶 — 线程池

文章目录线程池1 什么是线程池2 标准库中的线程池2.1 什么是工厂模式2.2 如何使用标准库中的线程池完成任务2.3 ThreadPoolExecutor 构造方法的解释3 实现一个线程池线程池 1 什么是线程池 随着并发程度的提高&#xff0c;随着对性能要求标准的提高会发现&#xff0c;好像线程…

[cpp进阶]C++异常

文章目录C语言传统处理错误的方式C异常概念C异常使用异常的抛出和捕获异常的重新抛出异常安全异常规范自定义异常体系C标准库的异常体系异常的优缺点C语言传统处理错误的方式 传统的错误处理机制&#xff1a; 终止程序。assert断言直接终止程序。缺点&#xff1a;过于粗暴&am…

Fiddler抓取手机APP报文

Http协议代理工具有很多&#xff0c;比如Burp Suite、Charles、Jmeter、Fiddler等&#xff0c;它们都可以用来抓取APP报文&#xff0c;其中charles和Burp Suite是收费的&#xff0c;Jmeter主要用来做接口测试&#xff0c;而Fiddler提供了免费版&#xff0c;本文记录一下在Windo…

位运算做加法,桶排序找消失元素,名次与真假表示,杨氏矩阵,字符串左旋(外加两道智力题)

Tips 1. 2. 3. 大小端字节序存储这种顺序只有在放进去暂时存储的时候是这样的&#xff0c;但是一旦我里面的数据需要参与什么运算之类的&#xff0c;会“拿出来”先恢复到原先的位置再参与运算&#xff0c;因此&#xff0c;大小端字节序存储的什么顺序不影响移位运算等等…

【案例教程】CLUE模型构建方法、模型验证及土地利用变化情景预测实践技术

【前沿】&#xff1a;土地利用/土地覆盖数据是生态、环境和气象等领域众多模型的重要输入参数之一。基于遥感影像解译&#xff0c;可获取历史或当前任何一个区域的土地利用/土地覆盖数据&#xff0c;用于评估区域的生态环境变化、评价重大生态工程建设成效等。借助CLUE模型&…

声音产生感知简记

声音产生 人的发音器官包括:肺、气管、声带、喉、咽、鼻腔、口腔、唇。肺部产生的气流冲击声带,产生震动。 声带每开启和闭合一次的时间是基音周期(Pitch period,T),其到数为基音频率(F.=1/T,基频),范围在70-450Hz。基频越高,声音越尖细,如小孩的声音比大人尖,就是…

编译错误2

本文迁移自本人网易博客&#xff0c;写于2015年11月25日&#xff0c;编译错误2 - lysygyy的日志 - 网易博客 (163.com)1、error C2059:语法错误&#xff1a;“<L_TYPE_RAW>”error C2238:意外的标记位于“;”之前.错误代码定位于&#xff1a;BOOL TreeView_GetCheckState…