一文带你清晰弄明白线程池的原理

不知道你是否还记得阿里巴巴的java代码规范中对多线程有这样一条强制规范:
【强制】线程资源必须通过线程池提供,不允许在程序中显示创建线程。
说明:使用线程池的好处是减少在创建和销毁线程池上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不适用线程池, 有可能造成系统创建大量同类线程而导致消耗完内存或者“过渡切换”的问题。

这条强制性规范也说明了使用线程池主要是解决一下两个问题:
(1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量一部任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
(2)线程管理: 每个java线程池会保持一些基本的线程统计信息, 以便对线程进行有效管理,使得能对所接收到异步任务进行高效调度。

JUC 的线程架构

JUC 是java.util.concurrent工具包的简称, 是从JDK1.5开始加入JDK的,用于完成高并发、处理多线程的一个工具包。 JUC 的线程架构如下图所示:
一文带你清晰弄明白线程池的原理_第1张图片
Executor是java 异步目标任务的“执行者”接口,其目的是执行目标任务。

ExecutorService继承于Executor。 它是java异步目标任务的“执行者服务接口”,对外提供异步任务的接收服务。

AbstractExecutorService 是一个抽象类,它的目的是为ExecutorService 中的接口提供默认实现。

ThreadPoolExecutor 是线程池的实现类, 是JUC线程池的核心实现类。

ScheduledExecutorService 是一个接口,可以完成延时和周期性任务调度线程池的接口,其功能类似Timer/TimerTask、

ScheduledThreadPoolExecutor提供了ScheduledExecutorService 线程池接口中“延迟执行”和周期执行等抽象调度方法的具体实现。在高并发程序中, ScheduledThreadPoolExecutor 的性能由于Timer。

Executors

Executors 是一个静态工厂类,它通过静态工厂方法返回ExecutorService、ScheduledExecutorService等线程池示例对象。 java通过Executors 工厂类可以提供4种快捷创建线程池的方法。如下图所示:
一文带你清晰弄明白线程池的原理_第2张图片
但是在阿里巴巴的java代码规范中,有这样一条强制规范:
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor 的方式。
说明: Executors返回的线程池对象的弊端如下:
(1)FixedThreadPool 和SingleThreadPool 运行的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
(2)CacheThreadPool 和ScheduledThreadPool 允许的创建线程数量为Integer.MAX_VALUE,可能 会创建大量的线程,从而导致OOM。

那么我们使用ThreadPoolExecutor 如何创建线程池呢,

ThreadPoolExecutor创建线程池

我们通过标准构造器ThreadPoolExecutor去构造工作线程池,Executors工厂类中创建线程池的快捷工厂方法实际上是调用TheadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的, 构造方法如下:

一文带你清晰弄明白线程池的原理_第3张图片
一文带你清晰弄明白线程池的原理_第4张图片
其中线程池执行器会根据corePoolSize和maximumPoolSize自动维护线程池中的工作线程,规则如下:
一文带你清晰弄明白线程池的原理_第5张图片
一文带你清晰弄明白线程池的原理_第6张图片

向线程池提交任务方式

向线程池提交任务的两种方式分别是execute()方法和submit()方法.
execute() 方法:
一文带你清晰弄明白线程池的原理_第7张图片

submit()方法:
一文带你清晰弄明白线程池的原理_第8张图片
从接口可以得出submit()和executor()方法的区别是:

  • Execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable,Runnale两种类型的类型, Callable类型的任务是可以返回执行结果的, Runnable类型的任务不可以返回执行结果.
  • submit()提交任务后会返回值,而execute()没有
  • submit()方便Exception处理.

线程池的任务调度流程

线程池的任务调度流程如下图所示:
一文带你清晰弄明白线程池的原理_第9张图片
在创建线程池的是,如果线程池的参数配置不合理,就会出现任务不能被正常调度的问题.

总结:
核心和最大线程数量,BlockingQueue队列等参数如果配置得不合理,可能会造成异步任务得不到预期的并发执行, 造成严重的排队等待现象.

想城池的调度器创建线程的一条重要的规则:在核心线程数已满后,还需要等待阻塞队列已满,才会去创建新的线程.

线程池的拒绝策略

当线程池的任务缓存队列为有界队列且阻塞队列已满的情况,提交任务到线程池的时候就会被拒绝, 当线程池已经被关闭了, 提交任务到线程池也会被拒绝. 无论是哪一种情况任务被拒绝, 线程池都会调用RejectedExecutionHandler拒绝策略接口的实例的rejectedExecution方法, 拒绝策略的实现有以下5中:

  • AbortPolicy-拒绝策略: 如果线程池队列已满,新任务就会被拒绝,并且抛出RejectedExecutionException异常, 该策略是线程池默认的拒绝策略.
  • DiscardPolicy-抛弃策略: 如果线程池队列已满, 新任务就会直接被丢掉, 并且不会有任何异常抛出.
  • DiscardOldestPolicy-抛弃最老任务策略: 如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,在尝试加入队列.
  • CallerRunsPolicy-调用者执行策略:在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务.
  • 自定义策略.实现RejectedExecutionHandler接口的rejectedExecution方法即可.

线程池关闭

一般情况下, 线程池启动后建议手动关闭,可以结合shutdown(),shutdownNow(),awaitTermination() 三个方法优雅地关闭一个线程池, 关闭步骤如下:
(1) 执行shutdow()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕.

(2) 执行awaitTermination(Long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成.

(3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务.

(4)补充执行awaitTermination(Long timeout,TimeUnit unit)方法,判断线程池是否关闭完成. 如果超时,就可以进入循环关闭,循环一定的次数,不断关闭线程池,直到其关闭或者循环结束.

关闭线程池代码如下:

public  static void shutdownThreadPoolGrecefully(ExecutorService threadPool){
        if (!(threadPool instanceof  ExecutorService)|| threadPool.isTerminated()){
            return;
        }
        try {
            //拒绝接收新任务
            threadPool.shutdown();
        }catch (SecurityException e){
            return;
        }catch (NullPointerException e){
            return;
        }
        try {
            //等待60s,等待线程池中的任务完成执行
            if(!threadPool.awaitTermination(60, TimeUnit.SECONDS)){
                //调用shutdowNow 取消正在执行的任务
                threadPool.shutdownNow();
                //再次等待60s ,如果还未结束,可以再次尝试,或则直接放弃
                if(!threadPool.awaitTermination(60,TimeUnit.SECONDS)){
                    System.out.println("线程池任务未正常完成执行结束");
                }
            }
        } catch (InterruptedException e) {
            // 捕获异常,重新调用shutdowNow
            threadPool.shutdownNow();
        }
        // 仍然没有关闭,循环关闭1000次,每次等嗲10毫秒
        if (!threadPool.isTerminated()){
            try {
                for (int i=0;i<1000;i++){
                    if (threadPool.awaitTermination(10,TimeUnit.SECONDS)){
                        break;
                    }
                    threadPool.shutdownNow();
                }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }catch (Throwable e){
                System.out.println(e.getMessage());
            }
        }
    }

除了以上的关闭线程池方法以外,还可以在JVM中注册一个钩子函数,在JVM进程关闭之前,由钩子函数自动将线程池关闭,以确保资源正常释放,代码如下:

  //懒汉式单例创建线程池:用于执行定时/顺序任务
    static class SeqOrScheduledTargetThreadPoolLazyHolder{
        //线程池:用于定时任务/顺序排队执行任务
        static  final  ScheduledThreadPoolExecutor EXECUTOR= new ScheduledThreadPoolExecutor(1,new CustomThreadFactory("seq"));
        static {
            //注册JVM关闭时的钩子函数
            Runtime.getRuntime().addShutdownHook(
                    new ShutdownHookThread("定时和顺序任务线程池",
                    new Callable(){

                        @Override
                        public Void call() throws Exception {
                          //关闭线程池
                            shutdownThreadPoolGrecefully(EXECUTOR);
                            return null;
                        }
                    }
            ));
        }
    }

到此为止,线程池的原理您明白了吗?

你可能感兴趣的:(java,jvm,java)