线程池

一、线程池综述

它的主要特点为:线程复用;控制最大并发数;管理线程。

使用线程池的好处有

1、降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁带来的资源消耗

2、提高响应速度。当任务到达时,任务可以不需要等到线程创建好就可以立即执行。

3、提高线程的可管理性。线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

二、线程池的架构

java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类

 

三、线程池的7大核心参数

1、corePoolSize:线程池中的常驻核心线程数

2、maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1

3、keepAliveTime:多余的空闲线程的存活时间。当前线程数量超过corePoolSize时,当空闲线程空闲时间达到keepAliveTime时,多余空闲线程会被销毁,直到只剩下corePoolSize个线程

4、unit:keepAliveTime的单位

5、workQueue:任务队列,被提交但尚未执行的任务

6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可

7、handler:拒绝策略,表示当任务队列满了并且工作线程大于等于线程池的最大线程数时如何拒绝请求执行的runnable的策略

四、线程池的工作流程

1、在创建了线程池后,等待提交过来的任务请求。

2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:

    2.1、如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

    2.2、如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

    2.3、如果这个时候队列满了且正在运行的线程数量还小于maximunPoolSize,那么还是要创建非核心线程来运行这个任务;

    2.4、如果这个时候队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务后,它会从队列取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:

    4.1、如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉;

    4.2、所以线程池的所有任务执行完成后它最终会收缩到corePoolSize的大小。

 五、线程池的拒绝策略

JDK内置了4中拒绝策略:

1、AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。

2、CallerRunsPolicy:调用者运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。

3、DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务。

4、DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。

以上拒绝策略均实现了RejectedExecutionHandler接口,我们也可以基于该接口实现自定义拒绝策略

六、线程池的使用方式

1、ExecutorService executorService1 = Executors.newFixedThreadPool(5);

    说明:创建了一个固定线程数为5的线程池,可控制线程最大并发数,超出的任务会在队列中等待,适用于执行长期任务

             corePoolSize和maximumPoolSize相等,使用LinkedBlockingQueue

    弊端:允许的队列长度为Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM

2、ExecutorService executorService2 = Executors.newSingleThreadExecutor();

    说明:创建了1个单线程的线程池,保证1个任务1个任务地顺序执行

             corePoolSize和maximumPoolSize都等于1,使用LinkedBlockingQueue

    弊端:允许的队列长度为Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM

3、ExecutorService executorService3 = Executors.newCachedThreadPool();

    说明:创建了1个可缓存线程池,适用于执行很多短时间的任务
             如果线程池线程数超过处理任务需要,可灵活回收空闲线程
             corePoolSize设置为0,maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue

    弊端:允许的创建线程数为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM

4、通过ThreadPoolExecutor创建线程,这也是《阿里巴巴Java开发规范》中要求的创建线程池的方式,好处是让写的同学能够能够更加明确线程池的运行规则,规避资源耗尽的风险。

public class ThreadPoolDemo {
    public static void main(String[] args) {
//        ExecutorService threadPool0 = Executors.newFixedThreadPool(5);
//
//        ExecutorService threadPool1 = Executors.newSingleThreadExecutor();
//
//
//        ExecutorService threadPool2 = Executors.newCachedThreadPool();

        ExecutorService threadPool3 = new ThreadPoolExecutor(
                2,
                5,
                1L,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        try {
            for (int i = 1; i <= 15; i++) {
                final int intI = i;
                threadPool3.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t处理第" + intI + "个任务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool3.shutdown();
        }
    }

控制台输出如下:

pool-4-thread-2 处理第2个任务
pool-4-thread-3 处理第6个任务
pool-4-thread-3 处理第3个任务
pool-4-thread-4 处理第7个任务
main 处理第10个任务
pool-4-thread-1 处理第1个任务
pool-4-thread-5 处理第8个任务
main 处理第14个任务
pool-4-thread-4 处理第9个任务
pool-4-thread-3 处理第5个任务
pool-4-thread-4 处理第13个任务
pool-4-thread-2 处理第4个任务
pool-4-thread-5 处理第12个任务
pool-4-thread-1 处理第11个任务
pool-4-thread-3 处理第15个任务

七、如何合理设置线程池参数

CPU密集型:指该任务需要大量的计算,而没有阻塞,CPU一直全速运行。CPU密集型任务只有在真正的多核CPU上才可能得到加速(通过多线程)。设置线程池的线程数为:CPU核数+1
IO密集型:该任务需要大量的IO,即大量的阻塞。用单线程运行IO密集型任务会导致大量CPU运算能力浪费在等待环节,所以针对IO密集型任务可以使用多线程来加速程序的运行。 N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务),瓶颈都在后端数据库访问或者RPC调用,本地CPU计算的时间很少,所以设置几十或者几百个工作线程是能够提升吞吐量的。

参考:https://mp.weixin.qq.com/s/EZawI9UGaTB3avnP6z-4_g

 
posted @ 2024-12-22 18:24  达摩克利斯之剑  阅读(56)  评论(0)    收藏  举报