未分类 · 2023年3月24日 0

JavaGuide知识点整理——线程池的最佳实践【】

重新声明下,虽然开这系列笔记的时候就说了这是最近看javaguide网站,然后为了加深记忆也为了好知识一起分享,所以把网站中的知识点搬运了一遍,其中掺杂这我自己的理解和实践等。然后各位如果感觉去可以去看原文。附上链接:https://snailclimb.gitee.io/javaguide/#/./docs/java/concurrent/java-thread-pool-best-practices

线程池在实际项目中的使用场景

线程池一般用于执行多个不相关联的耗时任务。没有多线程的情况下,任务使用顺序执行,使用了线程池的话可以让多个不相关联的任务同时执行。

假设我们要执行三个不相关的耗时任务。使用线程池前后的区别如下:

使用多线程前后

注意这里使用多线程执行不同的任务,可以用一个countDownLatch等待子任务执行完成才继续往下返回结果。

线程池最佳实践

使用ThreadPoolExecutor的构造函数声明线程池

线程池必须手动通过ThreadPoolExecutor的构造函数声明,避免使用Executors类的newFixedThreadPool和newCachedThreadPool,因为可能会有oom的风险。
说白了就是使用有界队列,控制线程创建数量。
除了避免OOM的原因外,不推荐使用Executors提佛那个的快捷线程池还有两个原因:

  1. 实际使用中需要根据自己机器的性能,业务场景来手动配置线程池的参数。比如核心线程数,最大线程数,使用的任务队列,拒绝策略等。
  2. 我们应该显示的给我们的线程池命名,这样有助于我们定位问题。

监测线程池运行状态

我们可以通过一些手段来检测线程池的运行状态,比如SpringBoot中的Actuator组件。
除此之外我们还可以通过ThreadPoolExecutor的相关api做一个简陋的监控。从下图可以看出,ThreadPoolExecutor提供了互殴线程池当前的线程数和活跃线程数,已执行完成的任务数,正在排队的任务数等任务。

ThreadPoolExecutor获取当前线程池信息

建议不同类别的业务用不同的线程池

很多人在实际项目中会有类似这样的问题:我的项目中多个业务需要用到线程池,是为每个线程池都定义一个还是说定义一个公共的线程池呢?
一般建议不同的业务使用不同的线程池, 配置线程池的时候根据当前业务情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重新优化系统性能瓶颈相关的业务。

下面我们看一个线程池运用不当的线上事故案例:

线程池使用不当demo

图中的代码可能会存在死锁的情况。为什么呢?下面我们捋一捋。
试想这么一个极端现象:如果核心线程数是n,父任务数量也为n。把核心线程全部占用。然后父任务下的子任务也需要用线程。在任务队列中阻塞等待获取线程。而父任务在等待子任务执行完成,子任务等待父任务释放线程资源好获取线程。也就造成了死锁。

解决方法也很简单,新增加一个用于执行子任务的线程池专门为其服务。

要给线程池命名

初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。
默认情况下创建的线程名字类似pool-1-thread-n这样的,没有业务含义,不利于我们定位问题。
给线程池里的线程命名通常有两种方式:

  1. 利用谷歌的ThreadFactoryBuilder给线程池里的线程命名
    public static void main(String[] args) throws Exception {
        ThreadFactory threadFactory =
            new ThreadFactoryBuilder().setNameFormat("用来测似的线程池" + "-%d").setDaemon(true).build();
       ThreadPoolExecutor threadPoolExecutor =
           new ThreadPoolExecutor(2,10,1l,TimeUnit.SECONDS,new ArrayBlockingQueue(10),threadFactory);
       for(int i = 0;i{
               System.out.println("当前线程名:"+Thread.currentThread().getName());
           });
       }
    }

如上代码就是命名了,下面是如果这个线程池里的线程报错,可以很容易定位。

定位到问题出自这个线程池
  1. 也可以自己实现ThreadFactor来给线程池里的线程命名
public class Test {

    public static void main(String[] args) throws Exception {
        MyThreadFactory threadFactory = new MyThreadFactory(Executors.defaultThreadFactory(),"测试线程池");
       ThreadPoolExecutor threadPoolExecutor =
           new ThreadPoolExecutor(2,10,1l,TimeUnit.SECONDS,new ArrayBlockingQueue(10),threadFactory);
       for(int i = 0;i{
               System.out.println("当前线程名:"+Thread.currentThread().getName());
           });
       }
    }
}

final  class MyThreadFactory implements ThreadFactory{

    ThreadFactory threadFactory;
    String name;
    AtomicInteger i = new AtomicInteger(1);

    MyThreadFactory(ThreadFactory threadFactory,String name){
        this.threadFactory = threadFactory;
        this.name = name;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = threadFactory.newThread(r);
        t.setName(name+i.getAndIncrement());
        return t;
    }
}

其实这种写法就是单纯的包了一层,每一个线程都手动的setName给设置了个名字。具体要用那种写法都可以,反正我是觉得自己设置的这个最开始写要麻烦点,但是每次用方便。谷歌的方法不需要创建什么工具类,但是每次创建都要设置。

正确的配置线程池参数

常规操作
首先这里要说一个常识:并不是线程越多越好。比如一个很小的任务,一个人做要1小时,但是60个人也不会是一分钟。甚至因为人多交流成本太大。指不定还会用时更长。
线程数量过多的影响也是和我们分配多少人做事一样。对于多线程的场景主要是增加了上下文切换成本。

类比我们人类通过合作做某件事,我们可以知道线程池过大过小都不好,合适才是最好的。
如果我们设置的线程池数量太小,同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至出现队列满了之后任务/请求无法处理的情况。或者大量任务堆积在队列中导致OOM,这样很明显是有问题的。CPU根本没有得到充分利用。
但是我们设置线程数量过大,大量线程可能同时争夺CPU资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响整体执行效率。

网上有一个简单并且通用的公式:

  • CPU密集型任务(n+1):这种任务消耗的是CPU资源,可以将线程数设置为cpu核心数+1.比CPU核心数多一个是为了防止线程偶发的缺页中断或者其他原因导致的任务暂停。一旦任务暂停,cpu就会处于空闲状态,这个时候多出的一个线程就可以充分利用CPU时间。
  • I/O密集型任务(2n):这种任务应用起来系统会占用大部分时间处理I/O交互,而线程处理I/O的时间段不会占用CPU来处理,这时候就可以把CPU交出给其他线程使用。所以我们可以多配置一些线程。比如2N。

如何判断是CPU密集任务还是IO密集任务?

CPU密集型简单理解就是利用CPU计算能力的任务。比如在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是IO密集型。这类任务的特点是CPU计算消耗时间相比于等待IO操作完成的时间来说很少。大部分时间都花费在等待IO操作完成上。

美团的骚操作

美团技术团队在java线程池实现原理以及在内图案业务中的实践这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。

美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是:

  • corPoolSize:核心线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize:当队列中存放的任务达到队列容量时,当前可以同事运行的线程数量变为最大线程数。
  • workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话新任务会被存放在队列中。
  • 还包括一些队列长度等。
动态修改线程池参数

实现的重点是基于ThreadPoolExecutor的几个方法,我们只需要维护ThreadPoolExecutor的实例,并在需要修改的时候拿到实例修改其参数即可。基于这个原理我们做到线程池参数的动态化,可视并且可配置。效果如下图。

动态修改线程池参数

其实我们基于这个思想可以做的就更多了。比如一些监控:线程池活跃度,告警,执行任务的频率和耗时,Reject异常等等。从而避免故障或者加速故障的修复。感觉美团技术团队对于线程池的实践介绍比较浅显易懂,感兴趣的可以自己去看下。

本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝大家工作顺顺利利,生活健健康康~!

打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录