0

JAVA线程池的使用 - 网无忌

 2 years ago
source link: https://www.cnblogs.com/netWild/p/16079369.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JAVA线程池的使用

一、使用 Executors 创建线程池

Executors是一个线程池工厂类,里面有许多静态方法,供开发者调用。

/* 该方法返回一个固定线程数量的线程池,该线程池池中的线程数量始终不变。 * 当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。 * 若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务  * 默认等待队列长度为Integer.MAX_VALUE */ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1); /* 该方法返回一个只有一个线程的线程池。 * 若多余一个任务被提交到线程池,任务会被保存在一个任务队列中,等待线程空闲,按先入先出顺序执行队列中的任务 * 默认等待队列长度为Integer.MAX_VALUE */ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); /*  * 该方法返回一个可根据实际情况调整线程数量的线程池。 * 线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。 * 若所有线程均在工作,又有新任务的提交,则会创建新的线程处理任务。 * 所有线程在当前任务执行完毕后,将返回线程池进行复用 */ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); /* 该方法返回一个ScheduledExecutorService对象,线程池大小为1。 * ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间内执行某任务的功能, * 如在某个固定的延时之后执行,或者周期性执行某个任务 */ExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor(); /* * 该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量 */ExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(1);

Executors 的静态方法都是基于 ThreadPoolExecutor 类实现的,相当于 ThreadPoolExecutor 的语法糖。

但这几个静态方法都存在一个弊端,因为会在创建线程池的同时隐式创建等待队列,而队列的长度默认是 Integer.MAX_VALUE ,相当于不限长度,这样就存在OOM的隐患。

二、使用 ThreadPoolExecutor 创建线程池

上面说过,Executors 的静态方法都是基于 ThreadPoolExecutor 类实现的,所以在生产环境下,还是建议直接使用 ThreadPoolExecutor 类创建线程池:

public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue<Runnable> workQueue);

ThreadPoolExecutor 有多个构造方法,一般来说使用最精简的即可。

三、参数含义

corePoolSize

指定线程池的核心线程数。

当一个新任务被添加到线程池时,首先会判断当前的线程数(ThreadCount),如果:

A:ThreadCount < corePoolSize:即当前线程数小于核心线程数,就会创建一个新的线程来执行这个任务;

B:ThreadCount >= corePoolSize:即当前线程数大于等于核心线程数,就会将新任务添加到等待队列中。

该参数的两个特殊参数值:

1、0:意味着没有核心线程,全部线程都会受到 keepAliveTime 参数的回收机制影响。

2、Integer.MAX_VALUE:意味着不限制核心线程数,连等待队列都不需要,可以想象这种情况下很容易OOM。

maximumPoolSize

指定线程池的最大线程数,包括核心线程和非核心线程。

当另一个新任务被添加到线程池时,如果此时等待队列的容量已满,则会判断当前的线程数(ThreadCount),如果:

A:ThreadCount < maximumPoolSize:即当前线程数小于最大线程数,就会创建一个新的线程来执行这个任务;

B:ThreadCount == maximumPoolSize:即当前线程数已达到最大值,此时等待队列的容量也已用尽,因此会抛出异常。

该参数的两个特殊参数值:

1、0:意味着只有核心线程,默认情况下全部线程都不会受到 keepAliveTime 参数的回收机制影响,除非设置 allowCoreThreadTimeOut 为 true。

2、Integer.MAX_VALUE:意味着不限制最大线程数,这种情况下也很容易OOM。

keepAliveTime

空闲线程的存活时间。

默认情况下,该参数只对非核心线程有效。

在处理大量任务时,可能会创建大量的非核心线程,在所有任务都执行完成后会继续保留这些非核心线程一段时间,等时间到了就会自动回收,以减少系统开销。

当设置线程池的 allowCoreThreadTimeOut(true) 时,意味着该参数也同时对核心线程有效,在时间到了之后,全部线程都会自动回收。

unit

空闲线程存活时间的单位。

workQueue

等待队列。

创建线程池时另外一个容易引起OOM的重要参数,主要包括以下几种:

1、ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO(先进先出)排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法 Executors.newFixedThreadPool() 使用了这个队列。
3、SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 LinkedBlockingQueue,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。
4、PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

以最常用的 LinkedBlockingQueue 为例:

//创建一个容量为9999的队列实例BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(9999);

关于线程池各参数的作用,可以通过下面的图片进行详细了解:

四、使用线程池的注意事项

一句话:应该最大化的,同时也要有限度的满足业务需求。

在实际使用线程池时,首先应该确保所创建的线程池可以满足业务设计需求,主要就是线程数和队列容量,前者由CPU核心数限制,后者由服务器内存限制。

线程太少,则消费队列的时间就长,就需要更大容量的队列;线程太多,会增加大量的上下文切换时间,反而不利于合理分配CPU的计算资源。

队列太小,则添加任务时可能会抛出异常;队列太大,会占用更多的内存消耗。

关键是切勿使用无边界值(Integer.MAX_VALUE),这也是造成OOM的最主要原因。

可以根据服务器配置和业务需求,对这两个方面进行均衡考虑。

五、使用案例

int cpuCoreCnt = Runtime.getRuntime().availableProcessors(); //获取服务器CPU核心数int corePoolSize = cpuCoreCnt;      // 核心线程数int maximumPoolSize = cpuCoreCnt;   // 最大线程数int keepAliveTime = 30;             // 非核心线程的空闲存活时长(分钟)int queueCapacity = 9999;           // 队列最大长度 BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(queueCapacity);ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, queue);threadPool.allowCoreThreadTimeOut(true);    //允许回收核心线程

上面案例中,使用CPU核心数作为最大线程数,相对来说还是比较合理的。

等待队列的容量尽可能设置的大一些,和添加任务时抛出异常相比,多付出一些内存来实现更大容量的队列还是非常值得的。

keepAliveTime 也可以适当设置的长一些,避免太快回收,毕竟频繁的创建线程也是需要时间开销的。

最后还设置了allowCoreThreadTimeOut方法,允许自动回收核心线程,用来减少阻塞线程的性能消耗。

六、线程池复用

线程池在完成全部的任务后,会自动进入摸鱼状态,期间会根据配置自动回收空闲线程,直到新的任务被添加进来再起来工作。

即使设置了 allowCoreThreadTimeOut(true) 对核心线程进行回收,有新任务时也会重新创建核心线程继续进入工作状态。

只要不是调用 shutdown() 手动关闭它,正常情况下线程池是可以长期重复性使用的。

有些强迫症患者(比如本人)会非常介意一个无所事事的线程池在内存里装死,因此必须手动 shutdown 才会安心。

但这样的话,之前的线程池就彻底挂掉了,再向其中添加任务时会抛出异常。

有效的做法是,将创建线程池的部分单独封装,每次添加任务时都进行判断,如果当前线程池已经挂掉了,就重新创建一个:

/** * <p> * 添加任务 * 注:如果线程池已关闭,会自动创建新的线程池 * </p> *  * @param task */public void addTask(Task task){	if(threadPool.isShutdown()) createThreadPool(corePoolSize, maximumPoolSize, keepAliveTime);	threadPool.execute(task);}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK