Java线程池使用
线程池的优点
当我们需要一个新的线程执行任务时,可能会直接创建一个
new Thread(()->{
// do something
}).start();
在业务量较少的情况,这样也没什么太大问题。
但是如果任务频繁的话
- 频繁的创建和销毁线程是十分消耗性能的,甚至可能创建和销毁线程所用时间大于任务本身执行所用时间
- 如果业务量非常大,可能会占用过多的资源,导致整个服务由于资源不足而宕机
这里就可以引入线程池。
线程池,简单来说,就是维护了若干个线程,当需要执行任务时,直接调用其中某一个线程来执行即可。
线程池的优点
- 降低性能消耗:重复利用已创建的线程,节省了频繁创建和销毁带来的性能损耗
- 提示任务效率:任务来了分配一个线程就可以立即干活,而不用等待线程重新创建
- 更好的管控:线程池可以控制线程数,避免过多消耗服务器资源;亦更方便调优和监控
线程池执行流程
先看看线程池核心构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
简单介绍下这7个参数
- corePoolSize: 核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:非核心线程存活时间
- unit:非核心线程存活时间单位
- workQueue:缓存队列
- threadFactory:线程工厂
- handler:拒绝策略
这里参数介绍非常简单,但足以理解线程池执行流程图,之后对这7个参数还会扩展说明,可结合理解。
实例类比
假设有一家xx银行,默认开放5个柜台营业,业务繁忙时最大可开放10个柜台。另外还有一个候客区,可容纳10个人。其运行流程大概如下
7大参数扩展说明
1 int corePoolSize
核心线程数
最初线程池里没有线程,一开始新建的就是核心线程
2 int maximumPoolSize
最大线程数
=核心线程数+非核心线程数
当线程数到了核心线程数且队列满了才会新建非核心线程
3 long keepAliveTime
非核心线程存活时间
非核心线程一段时间不干活就会被销毁
通常,核心线程闲置也会保留在线程池里。但如果设置ThreadPoolExecutor的allowCoreThreadTimeOut属性为true,核心线程闲置一段时间也会被销毁。
4 TimeUnit unit
非核心线程数存活时间单位
5 BlockingQueue workQueue
存放待执行任务的队列
5.1 SynchronousQueue
- 这个队列接收到任务的时候,会直接提交给线程处理,而不保留它。
- 如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务
- 使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE。(保证不出现【线程数达到了maximumPoolSize而不能新建线程】的错误)
5.2 LinkedBlockingQueue
- 这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务
- 如果当前线程数等于核心线程数,则进入队列等待
- 这个队列没有最大值限制,所有超过核心线程数的任务都将被添加到队列中。(这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize)
5.3 ArrayBlockingQueue
- 这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)执行任务
- 如果当前线程数等于核心线程数,则进入队列等待
- 如果队列已满,则新建线程(非核心线程)执行任务
- 如果总线程数到了maximumPoolSize,则触发拒绝策略
- 可以限定这个队列的长度
5.4 DelayQueue
- 这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
- 队列内元素必须实现Delayed接口,即任务必须实现Delayed接口
6 ThreadFactory threadFactory
线程工厂
这是一个接口,需要实现它的Thread newThread(Runnable r)方法
可以对线程进行自定义的初始化,例如给线程设定名字,方便后期调试
7 RejectedExecutionHandler handler
拒绝策略
当提交任务数超过maximumPoolSize+workQueue之和时触发
- AbortPolicy:默认策略。抛出RejectedExecutionException异常
- DiscardPolicy:丢弃当前任务,不抛出任何异常
- DIscardOldestPolicy:丢弃队列里最早添加的元素,再安排当前任务。如果失败则不断重试
- CallerRunsPolicy:使用调用者自己的线程来执行任务。调用者线程会调用执行器的execute方法来执行该任务
- 自定义策略:实现RejectedExecutionHandler接口,实现rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法
Java自带的四个线程池
了解了线程池7大参数,理解自带的四种线程池会容易很多
1 FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
固定大小的线程池
- 每次来新任务就会创建一个新的线程,直到线程数达到最大线程数(这里也是核心线程数)
- 线程数达到最大值后就不会变,因为没有非核心线程会被销毁
- 如果某个线程因为执行异常而结束,那么线程池会补充一个新线程
2 SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
一个单线程的线程池
- 该线程池只有一个线程工作,相当于单线程串行执行所有任务
- 保证所有任务的执行顺序按照任务的提交顺序执行
- 如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它
3 CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
一个可缓存的线程池
- 无核心线程,最大线程数Integer.MAX_VALUE
- 线程空闲60s后回收
- 缓存队列是不存储任务的队列
4 ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
一个有计划执行的线程池
- 有固定核心线程,最大线程数Integer.MAX_VALUE
- 定时以及周期性执行任务
自定义线程池
那么,我们实际中用哪一种捏。
答案是,都不用。
因为,上述线程中
- FixedThreadPool 和 SingleThreadExecutor 使用了 LinkedBlockingQueue ,这是个无界队列。当任务突发过多时,这个队列可能因为缓存太多任务而消耗非常多的内存资源,最终导致OOM
- CachedThreadPool 和 ScheduledThreadPool 最大线程数是Integer.MAX_VALUE。即相当于没有对最大线程数做限制,任务突发过多时,可能因为创建大量线程导致资源耗尽,最终同样导致OOM
所以,我们来自定义线程池。
线程池使用步骤
线程池使用步骤大概如下
- 根据7大参数,建立一个线程池
- 放入要执行的任务
- 最后,如有必要,关闭线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(params...);
pool.execute(Runnable r);
pool.shutdown();
其中关闭线程池有两个方法shutdown和shutdownNow,区别是
- shutdown: 线程池拒收新任务,等待线程池里的任务执行完毕,之后关闭线程池。
- shutdownNow: 线程池拒收新任务,线程池里的所有任务立刻停止,关闭线程池。
自定义线程池例子
1 自定义工具类。
包装自定义线程池,对外提供静态方法方便使用。
这里为了方便测试,核心线程1,最大线程10,缓存队列10,该线程池最大接收20个任务
package com.test.threadpool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CustomThreadPoolUtil {
private static Logger logger = LoggerFactory.getLogger(CustomThreadPoolUtil.class);
private static ThreadPoolExecutor pool = null;
static {
pool = new ThreadPoolExecutor(1, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), new CustomThreadFactory(), new CustomRejectedExecutionHandler());
}
public static void destory() {
pool.shutdown();
}
public static void execute(Runnable r) {
pool.execute(r);
}
private static class CustomThreadFactory implements ThreadFactory {
private AtomicInteger count = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
String threadName = CustomThreadPoolUtil.class.getSimpleName() + count.incrementAndGet();
t.setName(threadName);
return t;
}
}
private static class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
logger.error("任务执行失败 {}, 线程池已满 {}", r.toString(), executor.toString());
}
}
}
2 测试类。
创建21个任务,故意大于自定义线程池最大可处理量20
package com.test.threadpool;
public class TestThreadPool {
public static void main(String[] args) {
int num = 21;
for(int i=1; i<=num; i++) {
int j = i;
CustomThreadPoolUtil.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100); // 模拟业务运行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行了任务" + j);
}
});
}
CustomThreadPoolUtil.destory();
}
}
3 测试结果。
20个任务被正常执行。最后一个任务被拒绝,调用了我们的自定义拒绝方法
任务执行失败 com.threadpool.TestThreadPool$1@3af49f1c, 线程池已满 java.util.concurrent.ThreadPoolExecutor@19469ea2[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
CustomThreadPoolUtil2 执行了任务12
CustomThreadPoolUtil1 执行了任务1
CustomThreadPoolUtil7 执行了任务17
CustomThreadPoolUtil3 执行了任务13
CustomThreadPoolUtil4 执行了任务14
CustomThreadPoolUtil5 执行了任务15
CustomThreadPoolUtil9 执行了任务19
CustomThreadPoolUtil8 执行了任务18
CustomThreadPoolUtil6 执行了任务16
CustomThreadPoolUtil10 执行了任务20
CustomThreadPoolUtil1 执行了任务3
CustomThreadPoolUtil2 执行了任务2
CustomThreadPoolUtil9 执行了任务8
CustomThreadPoolUtil3 执行了任务5
CustomThreadPoolUtil7 执行了任务4
CustomThreadPoolUtil8 执行了任务9
CustomThreadPoolUtil5 执行了任务7
CustomThreadPoolUtil10 执行了任务11
CustomThreadPoolUtil6 执行了任务10
CustomThreadPoolUtil4 执行了任务6
参数设置推荐
刚才参数只是为了方便测试,实际中,如何设置各个参数才更合理呢
1 核心线程数(corePoolSize)
参考 任务耗时 和 每秒任务数
假设一个任务耗时0.1秒,系统每秒产生100个任务。
如果想在1秒内处理完这100个任务,那么有 0.1 * 100 / corePoolSize = 1,得 corePoolSize = 10
同理,如果只是偶尔某一秒产生了100个任务,后面有更多时间去处理,如2秒,那么0.1 * 100 / corePoolSize = 2,得 corePoolSize = 5
tip: 根据8020法则,实际应用中,不会每秒一直产生100的任务量,所以最终核心线程数可以设置为计算所得的80%,即最终corePoolSize = 10 * 0.8 = 8。而有时100的任务量,还有缓存队列和最大线程数来保证可以执行。不过为了方便后续计算,这里还是先取 corePoolSize = 10。
2 任务队列长度(workQueue)
参考 核心线程数 和 任务耗时
一般可设置为 核心线程数/单个任务执行时间*2
如本例中,缓存队列长度可设置为 10 / 0.1 * 2 = 200
3 最大线程数(maximumPoolSize)
参考 核心线程数,缓存队列长度,每秒最大任务数
一般可设置为 (最大任务数-任务队列长度)*单个任务执行时间
假设本例中,每秒最大任务数1000,则最大线程数 = (1000 - 200) * 0.1 = 80
4 最大空闲时间(keepAliveTime)
参考系统运行环境和硬件压力设定
无固定参考值,可根据系统产生任务的时间间隔合理设置
5 拒绝策略(handler)
参考 任务重要程度
任务不重要可直接丢弃,重要可自行采用缓冲机制
总结
本文先由单线程的弊端引出多线程,然后介绍了线程池的总体执行流程。
之后扩展说明7大参数,由此从理论上介绍了Java自带的四大线程池。
然而实际中并不使用它们,转到自定义线程池,并给出代码示例。最后介绍了自定义线程池的参数设置推荐。