JAVA线程池配置及使用注意事项

Posted 高级JAVA指南

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA线程池配置及使用注意事项相关的知识,希望对你有一定的参考价值。

本文主要介绍JAVA线程池相关的类层次结构、参数配置、线程池状态、线程池分类和线程池使用时的注意事项。

1. 线程池简介

JAVA中的线程池(java.util.concurrent.Executor系列) 是由Doug Lea设计,自JAVA1.5引入的线程管理和任务(实现Runnable的task)调度框架。

Doug Lea是何许人也?如果你经常翻看JDK的源码,应该会发现JDK中有不少代码的作者署名就是他。

线程池主要解决的问题:

1) 线程复用:通过线程复用减少反复创建线程的时间开销和内存开销;

2) 线程资源管控:限制并发线程数,控制空闲线程的存活时间,可避免机器的线程消耗殆尽或创建了大量的线程引起内存溢出等问题。

3) 任务调度:可通过改变线程池状态来管理线程池对正在执行的任务或请求队列中的任务以及新提交的任务的处理方式。


2. 线程池相关类介绍

先来看JDK线程池相关接口和类的层次关系:

线程池类层次结构图

java.util.concurrent.Executor是线程池的顶层接口,声明了线程池的基本职责即执行任务,就一个方法void execute(Runnable command)。

java.util.concurrent.ExecutorService继承自Executor,是java线程池框架的主要接口,ExecutorService不仅只是执行任务,还要对线程资源进行管控对任务进行调度,类结构如图:

ExecutorService的主要方法说明:

Future<?> submit(Runnable) 提交任务到线程池并返回Future

void shutdown() 停止任务提交,线程池不再接受新任务,继续运行正在执行中的任务及阻塞队列中的任务

List<Runnable> shutdownNow() 线程池不再接受新任务并抛弃阻塞队列中的任务,继续运行正在执行中的任务,返回阻塞队列(没有执行完成)中的任务列表

awaitTermination(long, TimeUnit) 如果已经shutdown了则阻塞当前主线程直到线程池中的任务执行完毕或超时,如果没有shutdown则会阻塞当前主线程直到超时

ThreadPoolExecutor是JAVA线程池的主要实现类(主角),可以理解为JAVA中的线程池就是指ThreadPoolExecutor,下面介绍我们的主角。


3. 线程池参数配置

ThreadPoolExecutor是java线程池框架(Executor-> ExecutorService)的主要实现类,其中定义了线程池的状态,线程池的任务调度策略,任务饱和策略,线程的创建和销毁策略等。

ThreadPoolExecutor任务调度基本思路:

1) 如果当前线程池线程个数小于corePoolSize则开启新线程执行提交的任务

2) 否则添加任务到任务队列(即阻塞队列java.util.concurrent.BlockingQueue,后面会有专门的文章介绍)

3) 如果任务队列满了,则尝试开启新线程执行任务,如果当前线程池中的线程数>maximumPoolSize则执行拒绝策略。

ThreadPoolExecutor(

    int corePoolSize,//核心线程数,允许同时执行任务的最大线程数

    int maximumPoolSize,// 最大线程数,允许同时处理任务的最大线程数

    long keepAliveTime,// 超出核心线程数的空闲线程的最大存活时间

    TimeUnit unit,//空闲线程存活时间单位

    BlockingQueue<Runnable> workQueue,// 阻塞任务队列,存储待执行的任务

    ThreadFactory threadFactory,// 线程工厂,用于创建线程,可指定线程命名规则

    RejectedExecutionHandler handler // 饱和策略(拒绝策略),当线程池阻塞队列已满时对新任务的处理

)


4. 线程池状态

ThreadPoolExecutor线程池状态列表:

RUNNING:接受新任务并且处理阻塞队列里的任务

SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务

STOP:拒绝新任务并且抛弃阻塞队列里的任务同时会中断正在处理的任务

TIDYING:所有任务都执行完(包含阻塞队列里面任务)当前线程池活动线程为0,将要调用terminated方法

TERMINATED:终止状态。terminated方法调用完成以后的状态

RUNNING -> SHUTDOWN:显式调用shutdown()方法,或者隐式调用了finalize(),它里面调用了shutdown()方法。


5. Executors线程池分类

java.util.concurrent.Executors是JDK自带的创建线程的工具类,查看该类的源码会发现其创建的线程池是ThreadPoolExecutor的实例。下面看下Executors创建线程池比较典型的4种方式:

1) newFixedThreadPool:创建一个核心线程个数和最大线程个数都为nThreads的线程池,并且阻塞队列长度为Integer.MAX_VALUE,keeyAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。可能引发的问题:阻塞队列太大会引起OOM;核心线程会常驻内存,可能会造成资源浪费。

2) newSingleThreadExecutor:创建一个核心线程个数和最大线程个数都为1的线程池,并且阻塞队列长度为Integer.MAX_VALUE,keeyAliveTime=0说明只要线程个数比核心线程个数多并且当前空闲则回收。缺点:单线程,可能会堆积大量待执行的任务。

3) newCachedThreadPool: 创建一个按需创建线程的线程池,最大线程数为Integer.MAX_VALUE,阻塞队列为最多只有一个任务的同步队列,keeyAliveTime=60只要线程60s内空闲则回收。问题点:最大线程数太大会引起OOM。

4) newScheduledThreadPool:创建一个最小线程个数corePoolSize,最大为Integer.MAX_VALUE,阻塞队列为DelayedWorkQueue的线程池。

Executors创建线程的方式虽然简单方便,但并不推荐使用此种方式。


6. 线程池使用注意事项

1) 建议使用new ThreadPoolExecutor(...)的方式创建线程池:

线程池的创建不应使用 Executors 去创建,而应该通过 ThreadPoolExecutor 创建,这样可以让读者更加明确地知道线程池的参数设置、运行规则,规避资源耗尽的风险,这一点在也阿里巴巴JAVA开发手册中也有明确要求。这一点不容小觑,曾有同学因为线程池使用不当导致生产的同一台机器上部署的多个应用都因无法创建线程池而出现故障。

2) 合理设置线程数:

线程池的工作线程数设置应根据实际情况配置,CPU密集型业务(搜索、排序等)CPU空闲时间较少,线程数不能设置太多。N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。

3) 设置能代表具体业务的线程名称:这样方便通过日志的线程名称识别所属业务。具体实现可以通过指定ThreadPoolExecutor的ThreadFactory参数。如使用spring提供的CustomizableThreadFactory。


以上是关于JAVA线程池配置及使用注意事项的主要内容,如果未能解决你的问题,请参考以下文章

并发编程精华问答| Java线程池使用时注意事项

java笔记java中线程池之ForkJoinPool的原理及使用

Java并发:线程池的使用

Java—线程池ThreadPoolExecutor案例详解,高薪必备

Java并发编程:线程及同步的性能——线程池

Android 线程池的封装