Booster 系列之——多线程优化

项目地址:github.com/didi/booste…

对于开发者来说,线程管理一直是最头疼的问题之一,尤其是业务复杂的 APP,每个业务模块都有着几十甚至上百个线程,而且,作为业务方,都希望本业务的线程优先级最高,能够在调度的过程中获得更多的 CPU 时间片,然而,过多的竞争意味着过多的资源浪费在了线程调度上。

如何能有效的解决上述的多线程管理问题呢?大多数人可能想到的是「使用统一的线程管理库」,当然,这是最理想的情况,而往往现实并非总是尽如人意。随着业务的高速迭代,积累的技术债也越来越多,面对错综复杂的业务逻辑和历史遗留问题,架构师如何从容应对?

在此之前,我们通过对线程进行埋点监控,发现了以下的现象:

  1. 在某种场景下会无限制的创建新线程,最终导致 OOM
  2. 在某一时间应用内的线程数达到数百甚至上千
  3. 即使在空闲的时候,线程池中的线程一直在 WAITING ,一直不会销毁

这些现象最终导致的问题是:

  1. OOM
  2. 无法分辨出线程所属的业务线,导致排查问题效率低下

针对这些问题,如果采用上面提到的「统一线程管理库」的方案,对于业务方来说,任何大范围的改造都意味着风险和成本,那有没有低成本的解决方案呢?经过反复思考和论证,最终我们选择了字节码注入方案,具体思路是:

  1. 对线程进行重命名

    重命名线程的主要目的是为了区分该线程是由哪个模块、哪个业务线创建的,这样,线程监控埋点的聚合能够做到更加精确

  2. 对线程池的参数进行调优

    • 限制线程池的 minPoolSizemaxPoolSize
    • 允许核心线程在空闲的时候自动销毁

线程重命名

经过分析发现,APP 中的线程创建主要是通过以下几种方式:

  • Thread 及其子类
  • TheadPoolExecutor 及其子类、ExecutorsThreadFactory 实现类
  • AsyncTask
  • Timer 及其子类

Thread 类为例,可以通过以下构造方法进行线程的实例化:

  • Thread()
  • Thread(runnable: Runnable)
  • Thread(group: ThreadGroup, runnable: Runnable)
  • Thread(name: String)
  • Thread(group: ThreadGroup, name: String)
  • Thread(runnable: Runnable, name: String)
  • Thread(group: ThreadGroup, runnable: Runnable, name: String)
  • Thread(group: ThreadGroup, runnable: Runnable, name: String, stackSize: long)

我们的目标就是将以上这些方法调用替换成对应的 ShadowThread 的静态方法:

  • ShadowThread.newThread(prefix: String)

    public static Thread newThread(final String prefix) {return new Thread(prefix);
    }
    复制代码
  • ShadowThread.newThread(target: Runnable, prefix: String)

    public static Thread newThread(final Runnable target, final String prefix) {return new Thread(target, prefix);
    }
    复制代码
  • ShadowThread.newThread(group: ThreadGroup, target: Runnable, prefix: String)

    public static Thread newThread(final ThreadGroup group, final Runnable target, final String prefix) {return new Thread(group, target, prefix);
    }
    复制代码
  • ShadowThread.newThread(name: String, prefix: String)

    public static Thread newThread(final String name, final String prefix) {return new Thread(makeThreadName(name, prefix));
    }
    复制代码
  • ShadowThread.newThread(group: ThreadGroup, name: String, prefix: String)

    public static Thread newThread(final ThreadGroup group, final String name, final String prefix) {return new Thread(group, makeThreadName(name, prefix));
    }
    复制代码
  • ShadowThread.newThread(target: Runnable, name: String, prefix: String)

    public static Thread newThread(final Runnable target, final String name, final String prefix) {return new Thread(target, makeThreadName(name, prefix));
    }
    复制代码
  • ShadowThread.newThread(group: ThreadGroup, target: Runnable, name: String, prefix: String)

    public static Thread newThread(final ThreadGroup group, final Runnable target, final String name, final String prefix) {return new Thread(group, target, makeThreadName(name, prefix));
    }
    复制代码
  • ShadowThread.newThread(group: ThreadGroup, target: Runnable, name: String, prefix: String)

    public static Thread newThread(final ThreadGroup group, final Runnable target, final String name, final long stackSize, final String prefix) {return new Thread(group, target, makeThreadName(name, prefix), stackSize);
    }
    复制代码

细心的读者可能会发现,ShadowThread 类的这些静态方法的参数比替换之前多了一个 prefix,其实,这个 prefix 就是调用 Thread 的构造方法的类的 className,而这个类名,是在 Transform 的过程中扫描出来的,下面用一个简单的例子来说明,比如我们有一个 MainActivity 类:

package com.didiglobal.booster.demo;public class MainActivity extends AppCompatActivity {public void onCreate(Bundle savedInstanceState) {new Thread(new Runnable() {public void run() {doSomething();}}).start();}}
复制代码

在未重命名之前,其创建的线程的命名是 Thread-{N},为了能让 APM 采集到的名字变成 com.didiglobal.booster.demo.MainActivity#Thread-{N},我们需要给线程的名字加一个前缀来标识,这个前缀就是 ShadowThread 的静态方法的最后一个参数 prefix 的来历。

线程池参数优化

理解了线程重命名的实现原理,线程池参数优化也就能理解了,同样也是将调用 ThreadPoolExecutor 类的构造方法替换为 ShadowThreadPoolExecutor 的静态方法,如下所示:

public static ThreadPoolExecutor newThreadPoolExecutor(final int corePoolSize, final int maxPoolSize, final long keepAliveTime, final TimeUnit unit, final BlockingQueue<Runnable> workQueue, final String name) {final ThreadPoolExecutor executor = new ThreadPoolExecutor(1, MAX_POOL_SIZE, keepAliveTime, unit, workQueue, new NamedThreadFactory(name));executor.allowCoreThreadTimeOut(keepAliveTime > 0);return executor;
}
复制代码

以上示例中,将线程池的核心线程数设置为 0,最大线程数设置为 MAX_POOL_SIZE[1],并且,允许核心线程在空闲时销毁,避免空闲线程占用过多的内存资源。

JDK Bug

经过以上对线程池的优化后中,我们信心满满的的准备灰度发布,但是,当我们在进行功耗测试时,发现 CPU 负载异常竟然高达 60%以上,经过一步步排查,最终发现问题出在 ScheduledThreadPoolminPoolSize 上,竟然命中了 JDK 的两个 bug,而且这两个 bug 直到 JDK 9 才修复:

  • JDK-8022642
  • JDK-8129861

这也就是为什么我们将 ScheduledThreadPoolminPoolSize 设置为了 1 的原因。

总结

针对多线程的优化主要是以下两个关键点:

  1. 将目标方法调用指令替换为注入的静态方法调用
  2. 在静态方法中构造优化过的线程、线程池实例并返回

当然,以上的优化方案比较偏保守,主要是考虑到尽可能降低优化带来的副作用,这也跟 APP 的应用场景有关,大家可以根据自身的业务需求进行相应的调整。


  1. MAX\_POOL\_SIZE = NCPU + 1↩︎

转载于:https://juejin.im/post/5cfcdd2ee51d455071250ae2

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

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

相关文章

OpenCL

OpenCL&#xff08;全称Open Computing Language&#xff0c;开放运算语言&#xff09;是第一个面向异构系统通用目的并行编程的开放式、免费标准&#xff0c;也是一个统一的编程环境&#xff0c;便于软件开发人员为高性能计算服务器、桌面计算系统、手持设备编写高效轻便的代码…

dubbo的底层原理

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一、Duboo基本概念解释 Dubbo是一种分布式服务框架。 Webservice也是一种服务框架&#xff0c;但是webservice并不是分布式的服务框架&…

BOM属性对象方法

本文原链接&#xff1a;https://cloud.tencent.com/developer/article/1018747 BOM 1.window对象 2.location对象 3.history对象 BOM也叫浏览器对象模型&#xff0c;它提供了很多对象&#xff0c;用于访问浏览器的功能。BOM缺少规范&#xff0c;每个浏览器提供商又按照自己想法…

nginx+php+mysql+haproxy+keepalived+NFS,搭建wordpress

实现LNMP 实现环境&#xff1a; 服务版本系统CentOS7.6Mysql5.6.34Nginx1.14.2PHP7.1.30HAProxy1.8.20Keepalived1.3.5NFS1.3.0主机IPMysql_master192.168.37.108Mysql_slave192.168.37.105NginxPHP192.168.37.103NginxPHP192.168.37.104HAProxyKeepalived192.168.37.101HAPro…

OpenCL “速成”冲刺【第一天】

话说软件开发从来没有速成一说&#xff0c;一门语言你学的越快&#xff0c;说明你在别的语言上下个功夫越多&#xff0c;所以这次加了引号&#xff0c;只不过几周之后可能会有一个公司内部OpenCL的考核&#xff0c;虽然本人不需要考核&#xff0c;不过也正好借机整理下之前Open…

Java8函数式编程

最近使用lambda表达式&#xff0c;感觉使用起来非常舒服&#xff0c;箭头函数极大增强了代码的表达能力。于是决心花点时间深入地去研究一下java8的函数式。 一、lambda表达式 先po一个最经典的例子——线程 public static void main(String[] args) {// Java7new Thread(new R…

电脑如何获得管理员权限

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 我只是记录下&#xff0c;方便以后查看。 参见&#xff1a; https://jingyan.baidu.com/article/ab69b270ff426e2ca6189f54.html

.NET混淆器 Dotfuscator如何保护应用程序?控制流了解一下!

Dotfuscator是一个.NET的Obfuscator。它提供企业级的应用程序保护&#xff0c;大大降低了盗版、知识产权盗窃和篡改的风险。Dotfuscator的分层混淆、加密、水印、自动失效、防调试、防篡改、报警和防御技术&#xff0c;为世界各地成千上万的应用程序提供保护。 Dotfuscator提供…

到底什么才是人生最大的投资

不是房子&#xff0c;不是股票&#xff0c; 是人&#xff0c;跟什么人交往&#xff0c;跟随什么人&#xff0c; 交什么样的朋友&#xff0c;其实就是你投资什么人&#xff0c; 而这&#xff0c;是对人生影响最大的。 钱不会给人机会&#xff0c;房子也不会&#xff0c; 只有人会…

tcpdump抓包命令

目录&#xff1a; 命令格式选项expression表达式示例【命令格式】 man手册显示如下 1 tcpdump [ -AbdDefhHIJKlLnNOpqStuUvxX# ] [ -B buffer_size ]2 [ -c count ]3 [ -C file_size ] [ -G rotate_seconds ] [ -F file ]4 [ -i …

百度Ueditor编辑器wordimage踩坑

背景 改造公司老项目后台编辑器&#xff0c;使用百度的Ueditor做替换。 发现问题 1、ue编辑器初始化后部分参数无法覆盖ueditor.config.js中的选项。2、wordimage&#xff08;word图片转存&#xff09;始终是灰色&#xff0c;无法使用。解决办法 1、将ueditor.config.js中的inp…

IntelliJ IDEA 配置JDK

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 IDEA配置JDK 1、点击File -->Project Structure&#xff1b; 2、点击左侧标签页SDKs选项&#xff0c;再点击左上角“”&#xff0c;…

get和post 两种基本请求方式的区别

GET和POST是HTTP请求的两种基本方法&#xff0c;要说它们的区别&#xff0c;接触过WEB开发的人都能说出一二。 最直观的区别就是GET把参数包含在URL中&#xff0c;POST通过request body传递参数。 你可能自己写过无数个GET和POST请求&#xff0c;或者已经看过很多权威网站总结出…

无论是工作还是生活都要记住这些话

1.如果你不喜欢现在的工作&#xff0c;要么辞职不干&#xff0c;要么就闭嘴不言。初出茅庐&#xff0c;往往眼高手低&#xff0c;心高气傲&#xff0c;大事做不了&#xff0c;小事不愿做。不要养成挑三拣四的习惯。不要雨天烦打伞&#xff0c;不带伞又怕淋雨&#xff0c;处处表…

苏嵌第一天,shell中一些基础知识

一、常用环境变量 1、HOME变量 Linux系统中的每个用户都有一个相关的称作HOME的目录。 2、PATH变量 包含一列用冒号定界的目录的路径名字&#xff0c;便于可执行程序的搜索。 3、PS1变量 PS1变量包含了shell提示符&#xff0c;$符号 4、LOGNAME变量 包含用户的注册名字…

Java异常处理001:Maven clean package时Failed to clean project: Failed to delete

Java异常处理001&#xff1a;Maven打包时Failed to clean project: Failed to delete 异常日志&#xff1a; [ERROR] Failed to execute goal org.apache.maven.plugins:maven-clean-plugin:2.6.1:clean (default-clean) on project fmk-web: Failed to clean project: Failed …

Weekly Contest 141

做了第一道后&#xff0c;看了下中间两道题目&#xff0c;没怎么看懂就先放着&#xff0c;做完最后一道&#xff0c;然后就没时间了。 1089. Duplicate Zeros Given a fixed length array arr of integers, duplicate each occurrence of zero, shifting the remaining element…

IntelliJ IDEA 中配置、使用 SVN

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1.配置svn 如下图&#xff1a; file -- setting -- version control -- subversion -- 选择 SVN安装路径 -- apply -- OK 2.直接检出…

切记!职场邮件需注意的细节

电子邮件是如今工作场所重要的通信工具之一&#xff0c;但不是每个人都知道如何很好地使用这个工具。工作邮件也是人际沟通的一种方式&#xff0c;和打电话、面谈一样&#xff0c;有很多学问讲究&#xff0c;所以在发送邮件之前一定要深思熟虑。 【发送&#xff0c;抄送&…

李洋疯狂C语言之初

1.sizeof 是看数据类型所占空间大小&#xff0c;这个大小是以 字节&#xff08;B&#xff09;为单位 char 是C语言的字符数据类型 %d 用在printf 中表示往屏幕打印一个数字 printf ("char&#xff1a; %d\n", sizeof(char)); 数据类型之间的关系&#xff0c;shor…