Java Executors四种线程池创建方式对比,解析如何科学的创建线程池

前言

本文主要阐述以下几个问题:

1、最基础创建线程的方式

2、Java Executors提供的四种线程池的说明及优缺点对比

3、最科学的线程池创建方式ThreadPoolExecutor

4、如何基于自己的机器合理的设置线程值


1、最基础创建线程的方式

一般开发中最简单的创建线程异步操作,new Thread()就行了

 private static void testNewThread() {
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            // new Thread() 每次都是新建一个线程去处理,循环10次可能就用了10个线程
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"打印输出i:"+ finalI);
            }).start();
        }
    }

但是线程无法统一管理,无疑是有很多弊端。

在一个高可用的应用系统中,一般是构建线程池来统一管理维护线程资源


2、Java Executors提供的四种线程池(方法、优缺点对比)

Java通过Executors提供四种线程池,分别为:

newSingleThreadExecutor 单一线程池,严格按顺序
newCachedThreadPool 长度无限大,用完的线程可灵活回收,适合耗时短的大量任务。
newFixedThreadPool 可重用固定线程数的线程池,超出的线程会在队列中等待。
newScheduledThreadPool 支持定时及周期性任务执行。

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行异步任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

   private static void testNewSingleThreadExecutor() {
        // newSingleThreadExecutor 一池只有一个线程,单线程按顺序串行执行所有任务
        // 只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
        // 如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+"打印输出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

运行结果如下,可以明显看到一直是同一线程在执行

pool-1-thread-1打印输出i:0
pool-1-thread-1打印输出i:1
pool-1-thread-1打印输出i:2
pool-1-thread-1打印输出i:3
pool-1-thread-1打印输出i:4
pool-1-thread-1打印输出i:5
pool-1-thread-1打印输出i:6
pool-1-thread-1打印输出i:7
pool-1-thread-1打印输出i:8
pool-1-thread-1打印输出i:9

newCachedThreadPool

newCachedThreadPool创建一个可缓存的线程池(线程长度为无限大),调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

    private static void testNewCachedThreadPool() {
        //创建一个可缓存线程池,完成任务后的空闲线程可灵活回收,若无可回收,则新建线程。
        //newCachedThreadPool的线程池数量为无限大,可能会创建数量非常多的线程,甚至OOM
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+"打印输出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

打印结果,可以看到,完成任务的线程会被重复利用

pool-1-thread-1打印输出i:0
pool-1-thread-2打印输出i:1
pool-1-thread-3打印输出i:2
pool-1-thread-4打印输出i:3
pool-1-thread-7打印输出i:6
pool-1-thread-7打印输出i:9
pool-1-thread-8打印输出i:7
pool-1-thread-2打印输出i:8
pool-1-thread-5打印输出i:4
pool-1-thread-6打印输出i:5

**弊端:**newCachedThreadPool的线程池数量为无限大,可能会创建数量非常多的线程,甚至OOM

newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

   private static void testNewFixedThreadPool() {
        //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
        //堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+"打印输出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

可以看到,设置长度为2的情况下,任务会等待完成后继续执行

pool-1-thread-1打印输出i:0
pool-1-thread-2打印输出i:1
pool-1-thread-1打印输出i:2
pool-1-thread-2打印输出i:3
pool-1-thread-1打印输出i:4
pool-1-thread-2打印输出i:5
pool-1-thread-1打印输出i:6
pool-1-thread-2打印输出i:7
pool-1-thread-1打印输出i:8
pool-1-thread-2打印输出i:9

newScheduledThreadPool

newScheduledThreadPool主要用于支持定时及周期性任务执行。线程池长度可指定(适合一些简单的定时或周期性任务,一般任务模块的稳定性实现可以使用xxl_job | quartz)

延迟执行

    private static void testNewScheduledThreadPool() throws InterruptedException {
        //创建一个定长线程池,支持定时及周期性任务执行。
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
//        // 延迟执行 以下语句延迟3秒再打印
        scheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("this print delay 3 seconds");
            }
        }, 3, TimeUnit.SECONDS);
    }

周期执行

 private static void testNewScheduledThreadPool() throws InterruptedException {
        // 周期定时执行,延迟1秒后每3秒执行一次
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                logger.info("delay 1 seconds, and excute every 3 seconds");
            }
        }, 1, 3, TimeUnit.SECONDS);
    }

输出结果如下

2022-03-04 16:30:50,805 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds
2022-03-04 16:30:53,805 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds
2022-03-04 16:30:56,803 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds
2022-03-04 16:30:59,804 [study.thread.ThreadAnalysis]-[INFO] delay 1 seconds, and excute every 3 seconds

Executors提供的四种线程池虽各有优缺点,但总体而言不是最科学的线程池管理方式,在阿里发布的编程规约中明确有以下说明:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

我们阅读Executors四种线程池底层源码可以发现底层其实也是hreadPoolExecutor来创建的,推荐用ThreadPoolExecutor来构建管理线程池


3、最科学的线程池创建方式ThreadPoolExecutor

讲ThreadPoolExecutor首先要分析它的构造函数,**构造函数和他的参数理解了,java线程池也就玩明白了

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler
)


工作策略如下:

 

接下来我们实际构造一个ThreadPoolExecutor线程池,分析其各个参数的含义

 private static void testThreadPoolExecutor(){
        //创建工作队列,ArrayBlockingQueue限定队列大小,指定容量(线程池和队列都满了后执行拒绝策略)
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(5);
//        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(5);
        ExecutorService threadPool = new ThreadPoolExecutor(
                // 核心线程数,如果运行线程数小于corePoolSize,提交新任务时就会新建一个线程来运行
                //如果运行线程数大于或等于corePoolSize,新提交的任务就会入列等待;
                2,
                // 最大线程数
                //如果等待队列已满,并且运行线程数小于maximumPoolSize,也将会新建一个线程来运行;
                //如果线程数大于maximumPoolSize,新提交的任务将会根据拒绝策略(RejectedExecutionHandler handler)来处理
                4,
                // keepAliveTime为超过corePoolSize线程数量的线程指定最大空闲时间,unit为时间单位
                60L, TimeUnit.SECONDS,
                //队列,用于存放提交的等待执行任务
                // 入队策略分为三种:有界队列(eg:ArrayBlockingQueue),无界队列(eg:LinkedBlockingQueue),直接传递(eg:SynchronousQueue)
                workQueue,
                // 线程工厂,我们还可以自定义线程工厂来设置线程信息,如给线程统一设置前缀改名
                Executors.defaultThreadFactory(),
                // 指定拒绝策略
                // 拒绝策略默认提供四种,比较温和的策略:AbortPolicy(在需要拒绝任务时抛出RejectedExecutionException),可自定义实现拒绝策略
                new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10 ; i++) {
            int finalI = i;
            threadPool.execute(()->{
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"打印输出i:"+ finalI);
            });
        }
        threadPool.shutdown();
    }

打印结果如下,可以看到,在最大线程数量maximumPoolSize限制为4,且有界队列ArrayBlockingQueue容

量只为5时,执行10个线程任务,线程池达到饱和,有一个线程即因失败抛出了RejectedExecutionException异常

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task study.thread.ThreadAnalysis$$Lambda$1/990398217@614ddd49 rejected from java.util.concurrent.ThreadPoolExecutor@1c3a4799[Running, pool size = 4, active threads = 4, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at study.thread.ThreadAnalysis.testThreadPoolExecutor(ThreadAnalysis.java:128)
	at study.thread.ThreadAnalysis.main(ThreadAnalysis.java:27)
pool-1-thread-1打印输出i:0
pool-1-thread-3打印输出i:7
pool-1-thread-4打印输出i:8
pool-1-thread-2打印输出i:1
pool-1-thread-4打印输出i:4
pool-1-thread-2打印输出i:5
pool-1-thread-1打印输出i:2
pool-1-thread-3打印输出i:3
pool-1-thread-4打印输出i:6

关于队列和拒绝策略的几种不同形式要注意区分

默认三种不同的BlockingQueue队列

newCachedThreadPool使用的是SynchronousQueue(我简单理解,线程无限大也就不需要队列,直接传递)
newSingleThreadExecutor和newFixedThreadPool使用的是LinkedBlockingQueue(除非系统资源耗尽,否则无界的任务队列不存在任务入队失败)
newScheduledThreadPool使用的就是自定义实现的DelayedWorkQueue

**     这里要注意使用无界队列LinkedBlockingQueue时,maximumPoolSize就失去了作用**

4种不同的拒绝策略

可以看出AbortPolicy策略比较温和,会抛出异常,为系统稳定性考虑建议默认使用这个


4、怎么基于自己的机器合理的设置线程?

线程池介绍完了,那么最后我们如何根据项目的不同情况和机器配置来设置最优线程池呢?

一般来说有两种衡量方式:

CPU密集型:corePoolSize = CPU核数 + 1 (eg:线程大多去执行一些计算任务之所以 +1 是考虑充分利用 cpu 资源, 避免空闲

IO密集型:corePoolSize = CPU核数 * 2 (eg: 线程大多去执行数据库写操作、发消息等因为io密集型瓶颈不在cpu, 所以可以多开些线程

cpu核数计算

// 打印出本机可用cpu核数
System.out.println(Runtime.getRuntime().availableProcessors());

linux服务器的cpu核数计算(考虑到物理cpu和超线程技术,一般以逻辑cpu核数为准)

1.核数和逻辑CPU计算公式
核数 = 物理CPU个数 * 每颗物理CPU的核数
逻辑CPU数 = 物理CPU个数 * 每颗物理CPU的核数 * 超线程数

(1)查看物理CPU个数
# grep "physical id" /proc/cpuinfo | sort | uniq| wc -l
(2)查看每个物理CPU中core的个数(即核数)
# grep "cpu cores" /proc/cpuinfo | uniq
(3)查看逻辑CPU的个数,推荐使用
# grep "processor" /proc/cpuinfo | wc -l

示例

最后来个实际项目的线程池配置示例吧

linux服务器的可用逻辑cpu核数为16个(IO密集型任务,coreSize=cpu核数*2),我的线程池创建策略如下:

 @Bean
    ExecutorService executorService(){
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(128);
        return new ThreadPoolExecutor(32, 64, 60L, TimeUnit.SECONDS,
                workQueue, Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    }

参考链接

Java 四种线程池newCachedThreadPool,newFixedThreadPool,newScheduledThreadPool,newSingleThreadExecutor

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式

有界、无界队列对ThreadPoolExcutor执行的影响

常见linux服务器物理CPU个数逻辑CPU个数计算方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章