创建线程池的核心方式 --- ThreadPoolExecutor

Posted Putarmor

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建线程池的核心方式 --- ThreadPoolExecutor相关的知识,希望对你有一定的参考价值。

ThreadPoolExecutor重点知识

在这里插入图片描述

创建线程池的方式总共有7种,前6种我们基本是不用的,原因是什么呢?

阿里巴巴Java开发手册中,强制表明了线程池不允许使用Excutors去创建,而是通过ThreadPoolExecutor的方式处理,这样去避免资源耗尽的危险:
Executors返回的线程池对象弊端如下:
1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,这样可能会堆积大量的请求,从而导致内存溢出(OOM)。
2)CachedThreadPool:
允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

1.创建线程池示例

public static void main(String[] args) {
        //原始创建线程池方法
        //5是正式员工数量  10为正式员工和临时员工最大值   60s是临时员工最大生命周期
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,60, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000));   //给任务队列一定要设置容量,否则默认是在最大值 Integer.MAX_VALUE
        for(int i = 0; i < 5; i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }

在这里插入图片描述
程序中一共具有5个任务,从运行结果看出线程池创建了5个线程

当程序中任务为2个时:

在这里插入图片描述

2.线程池懒加载机制

有任务的时候才会线程池才会创建线程,而不是一开始就会创建线程,当任务小于线程池设定线程数量时,只会创建任务数量大小的线程数量。


当程序种任务为6个时:
public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,60, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000));   
        for(int i = 0; i < 6; i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }

在这里插入图片描述
从执行结果可以看出,6个任务但是打印出的线程名只有5个,实际上最后一个任务被放在任务队列中,后面被有空的核心线程执行,可以说是线程池中线程复用了。


3.设置线程池中线程名

通过ThreadFactory实现自定义设置线程名称

public class ThreadPool3 {
    private static int id = 1;
    public static void main(String[] args) {
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);   //创建线程后一定记得把Runnable任务传给线程
                thread.setName("myThread-"+id++);
                return thread;
            }
        };
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,5,0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000),threadFactory);

        //执行任务
        for(int i = 0; i < 5; i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }
}

在这里插入图片描述

4.线程池的拒绝策略

jdk提供了4种拒绝策略,此外可以自定义拒绝策略,可以说一共有5种

第一种默认拒绝策略:
new ThreadPoolExecutor.AbortPolicy()也是默认拒绝策略的执行结果

public static void main(String[] args) {

        //默认拒绝策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,
                0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(5)); //5是任务队列长度

        for(int i = 0; i < 11; i++){
            int id = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + id + "线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }

在这里插入图片描述

从程序执行结果发现,抛出了异常,一共执行了10次任务,最后一次任务被抛弃;原因:当出现11个任务时,首先创建了5个核心线程,然后将其余6个任务中的前5个放在任务队列中,由于线程池最大线程数量为5,所以最后一个任务就要被舍弃掉。

第二种:使用调用线程池的线程(主线程)执行任务
new ThreadPoolExecutor.CallerRunsPolicy()

public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5,5,
                0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(5),new ThreadPoolExecutor.CallerRunsPolicy());
        for(int i = 0; i < 11; i++){
            int id = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + id + "线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }

在这里插入图片描述
第三种:new ThreadPoolExecutor.DiscardPolicy()
程序执行不报错,忽略新的任务
在这里插入图片描述

第四种:忽略老任务(最先加入到任务队列的任务)
new ThreadPoolExecutor.DiscardOldestPolicy()
在这里插入图片描述
第五种:自定义拒绝策略

public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5,
                0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(5),
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        //自定义拒绝策略
                        System.out.println("执行了自定义拒绝策略");
                    }
                });
        for(int i = 0; i < 11; i++){
            int id = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:" + id + "线程名:"+Thread.currentThread().getName());
                }
            });
        }
    }

在这里插入图片描述
从执行结果来看,第11(编号为10)个任务执行了自定义拒绝策略

5.ThreadPoolExecutor参数解析

ThreadPoolExecutor构造方法一共有4种,每种构造方法的参数个数不一致,通过第四种构造方法,我们解析一下每个参数代表的意义。
在这里插入图片描述
第一个参数:线程池中核心线程的数量(快递站的正式员工)
第二个参数:线程池中最大线程数量(快递站正式员工和临时员工的数量和)
第三个参数:临时员工的最大生命周期(当没有任务执行时,临时线程经过该时间后销毁)
第四个参数:为第三个参数服务,代表时间单位
第五个参数:线程池中的任务队列,一定要设置队列大小,否则默认Integer.MAX_VALUE
第六个参数:线程工厂(可以设置自定义线程名称、设置线程优先级、设置守护线程等)
第七个参数:设置线程池拒绝策略

5.线程池的运行原理(重中之重)

了解线程池的执行流程或者说运行原理对于我们了解并发编程的核心是至关重要的,相信下面这副流程图一定让你对线程池运行有个清晰的认知。
在这里插入图片描述

6.线程池的运行方式

线程池的运行方式包含两种,分别是execute()和submit():

①execute()
无返回值就不进行举例了,执行的大多数任务都是无返回值的。

②submit()

public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,5,0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000)
        );

        //拿到返回值
        Future<Integer> future = executor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception{
                int num = new Random().nextInt(10);
                System.out.println("生成的随机数:"+num);
                return num;
            }
        });
        System.out.println("main得到返回值:"+future.get());
    }

在这里插入图片描述
二者区别:
1.execute只能执行Runnable的任务,也就是说它运行的程序没有返回值;而submit既能执行Runnable无返回值的任务,也能执行Callable有返回值的任务。(最本质区别)
2.execute执行过程中出现OOM(内存溢出)时会将异常打印到控制台;而submit运行出现OOM不会打印异常信息(原因:try/catch包裹)。

7.线程池终止方式

线程池的特点:线程池相对于线程来说具有长生命周期;对于之前使用的线程而言,当它执行完任务之后就会销毁,而对于线程池来说,线程执行任务后,创建的线程不会销毁,线程会处于waiting状态,线程池一直处于运行状态并等待新的任务。

线程池的终止方式包含两种,分别是shutdown()和shutdownNow()

①shutdown()

public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,10,0,
                TimeUnit.SECONDS,new LinkedBlockingDeque<>(1000)
        );

        for(int i = 0; i < 100; i++){
            int id = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(String.format("编号:%d, 线程名:%s",
                            id,Thread.currentThread().getName()));
                }
            });
        }
        executor.shutdown();
    }

在这里插入图片描述
从执行结果可以看出,100次任务全部执行了(由于运行结果太长,截取了中间部分)

②shutdownNow()
在这里插入图片描述
从执行结果可以看出,只执行了10次任务

二者区别:
1.shutdown拒绝新任务加入;等待线程池中任务队列中任务执行结束后再停止线程池
2.shutdownNow拒绝执行新任务;不会去等待任务队列中的任务执行,就停止线程池

8.线程池的状态都有哪些?

线程具有6种状态,从ThreadPoolExecutor源码种我们发现线程池为5种状态:
在这里插入图片描述
RUNNING:运行状态(正常状态)
SHUTDOWN:执行shutdown()后的状态
STOP:执行shutdownNow()后的状态
TIDYING:清空线程池线程后的状态(稍纵即逝)
TEMINATED:线程池销毁

线程池状态转移图:
在这里插入图片描述
注意:线程池的状态只是供开发者使用,对于客户机是不透明不可见的。


老铁们点个👍,谢谢😊!

以上是关于创建线程池的核心方式 --- ThreadPoolExecutor的主要内容,如果未能解决你的问题,请参考以下文章

Java核心深入理解线程池ThreadPool

线程池ThreadPoolExecutor

.net线程池内幕

ThreadPool

创建线程池的核心方式 --- ThreadPoolExecutor

[Java] Java核心深入理解线程池ThreadPool