「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」
为什么使用线程池
在Java程序中,我们常常是在收到一个任务之后就创建一个线程去执行,但是如果任务比较多的时候,我们就需要频繁地创建和销毁线程,对资源消耗比较多,同时响应速度也比相对较慢。
为了避免频繁地创建和销毁线程而浪费资源,同时提高响应速度,我们可以使用线程池来优化。
线程池优势
线程池就是把线程放到一个“池子”里,需要执行任务的时候就从池子里选择一个线程来执行,使用线程池有以下优势:
- 降低资源消耗:通过直接去池子里获取线程减少了线程频繁创建和销毁的开销;
- 提高响应速度:任务到来时无需等待创建线程,直接从池子中取;
- 提高线程可管理性:可以管理线程,避免出现创建过多线程,可以根据系统特性设置参数进行调优、监控。
ThreadPoolExecutor类
java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类,它有四个构造方法:
public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
构造方法的相关参数详解
- corePoolSize:核心线程数,线程池创建完成后,线程池中的线程数量为0,当有任务来时才创建新线程放到线程池中。
- maximumPoolSize:最大线程数量,表示线程池最大可以容纳的线程数量,当线程数量达到corePoolSize且等待队列workQueue已经满时,如果还有新任务到来,还没达到maximumPoolSize时,会再创建线程放到线程池中。
- keepAliveTime:线程空闲时存活时间
- unit:枚举类型,keepAliveTime的单位,有以下几个枚举值
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小时
- TimeUnit.MINUTES; //分钟
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //纳秒
- workQueue:阻塞队列,用来暂存等待执行的任务,常用的选择有:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
- PriorityBlockingQueue :支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的无界阻塞队列。
- threadFactory:创建线程的工厂,用于创建线程。
- handler:拒绝策略,当线程池的任务队列达到maximumPoolSize时,新任务到来就会采取拒绝策略,有以下四种:
- ThreadPoolExecutor.AbortPolicy:丢弃任务抛出异常;
- ThreadPoolExecutor.DiscardPolicy:丢弃任务但不抛异常;
- ThreadPoolExecutor.DiscardOldPolicy:每次新任务来时,丢弃等待队列最前面的任务;
- ThreadPoolExecutor.CallerRunsPolicy: 任务创建线程执行。
在ThreadPoolExecutor类中有几个非常重要的方法
-
execute():提交任务交由线程池去执行。
-
submit():与execute()方法一样,向线程池提交一个任务,由线程池去执行。该方法声明与ExecutorService中,在AbstractExecutorService中进行了实现,在ThreadPoolExecutor中没有对其进行重写,直接调用父类的submit(),与execute()方法不同的是,它能返回任务执行的结果,使用Future来获取任务执行结果。
-
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。
-
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
线程池状态
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
ctl
包含两部分信息,运行状态和线程数量,高3位报错runState,低29位保存workCount。
线程池状态共有五种:
- RUNNING:能接受新任务也能处理阻塞队列里的任务;
- SHUTDOWN:调用shutdown()进入该状态,关闭状态,不再接受新任务,可以继续处理阻塞队列中已有的任务;
- STOP:调用shutdownNow()进入该状态,不接受新任务也不处理任务,同时会中断正在处理的线程;
- TIDYING:所有任务都已终止,workCount为0;
- TERMINATED:死亡状态,在terminated()方法执行后进入该状态。
实例
创建一个线程池:
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,20,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10));
核心线程数为5,最大线程数为10,空闲存活时间为20秒,阻塞队列为LinkedBlockingQueue,大小为10。
创建一个任务类MyTask,实现了Runnable接口,重写run()方法。
class MyTask implements Runnable{
private final int taskNum;
public MyTask(int num){
this.taskNum=num;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在执行任务:"+taskNum);
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务"+taskNum+"执行完毕!");
}
}
循环提交任务
for (int i=0;i<15;i++){
MyTask task = new MyTask(i);
executor.execute(task);
System.out.println("线程池中线程数量:"+executor.getPoolSize()
+", 队列中等待的的任务数量:" +executor.getQueue().size()
+",已执行完毕的任务数目:"+executor.getCompletedTaskCount());
}
完整代码:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test01 {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,
5,
20,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10));
for (int i=0;i<15;i++){
MyTask task = new MyTask(i);
executor.execute(task);
System.out.println("线程池中线程数量:"+executor.getPoolSize()
+", 队列中等待的的任务数量:" +executor.getQueue().size()
+",已执行完毕的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable{
private final int taskNum;
public MyTask(int num){
this.taskNum=num;
}
@Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在执行任务:"+taskNum);
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务"+taskNum+"执行完毕!");
}
}
运行结果:
线程pool-1-thread-1正在执行任务:0
线程池中线程数量:1, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程池中线程数量:2, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程池中线程数量:3, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-2正在执行任务:1
线程pool-1-thread-3正在执行任务:2
线程池中线程数量:4, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-4正在执行任务:3
线程池中线程数量:5, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-5正在执行任务:4
线程池中线程数量:5, 队列中等待的的任务数量:1,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:2,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:3,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:4,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:6,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:7,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:8,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:9,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:10,已执行完毕的任务数目:0
任务2执行完毕!
任务0执行完毕!
任务3执行完毕!
任务1执行完毕!
任务4执行完毕!
线程pool-1-thread-2正在执行任务:8
线程pool-1-thread-4正在执行任务:7
线程pool-1-thread-1正在执行任务:6
线程pool-1-thread-3正在执行任务:5
线程pool-1-thread-5正在执行任务:9
任务8执行完毕!
任务9执行完毕!
线程pool-1-thread-2正在执行任务:10
任务7执行完毕!
任务5执行完毕!
任务6执行完毕!
线程pool-1-thread-3正在执行任务:13
线程pool-1-thread-4正在执行任务:12
线程pool-1-thread-5正在执行任务:11
线程pool-1-thread-1正在执行任务:14
任务10执行完毕!
任务13执行完毕!
任务11执行完毕!
任务14执行完毕!
任务12执行完毕!
执行结果分析:
线程池核心线程数为5,所以任务0,1,2,3,4分别由线程池创建线程执行,任务5-14进入阻塞队列,阻塞队列中等待的任务数量为10,当任务0,1,2,3,4执行完毕,任务5,6,7,8,9依次由线程pool-1-thread-1—pool-1-thread-5执行,具体由哪一个线程顺序不确定,任务5-9执行完毕再执行阻塞队列中的任务10-14。
如果把阻塞队列大小改为5
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,20,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5));
执行结果:
线程pool-1-thread-1正在执行任务:0
线程池中线程数量:1, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程池中线程数量:2, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-2正在执行任务:1
线程池中线程数量:3, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-3正在执行任务:2
线程池中线程数量:4, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-4正在执行任务:3
线程池中线程数量:5, 队列中等待的的任务数量:0,已执行完毕的任务数目:0
线程pool-1-thread-5正在执行任务:4
线程池中线程数量:5, 队列中等待的的任务数量:1,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:2,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:3,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:4,已执行完毕的任务数目:0
线程池中线程数量:5, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程池中线程数量:6, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程池中线程数量:7, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-6正在执行任务:10
线程池中线程数量:8, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-7正在执行任务:11
线程池中线程数量:9, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-8正在执行任务:12
线程pool-1-thread-9正在执行任务:13
线程池中线程数量:10, 队列中等待的的任务数量:5,已执行完毕的任务数目:0
线程pool-1-thread-10正在执行任务:14
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task testThread.MyTask@12a3a380 rejected from java.util.concurrent.ThreadPoolExecutor@29453f44[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at testThread.Test01.main(Test01.java:16)
任务13执行完毕!
任务3执行完毕!
任务2执行完毕!
任务1执行完毕!
任务12执行完毕!
任务11执行完毕!
任务10执行完毕!
任务14执行完毕!
任务0执行完毕!
任务4执行完毕!
线程pool-1-thread-8正在执行任务:9
线程pool-1-thread-2正在执行任务:8
线程pool-1-thread-4正在执行任务:7
线程pool-1-thread-3正在执行任务:6
线程pool-1-thread-9正在执行任务:5
任务8执行完毕!
任务9执行完毕!
任务7执行完毕!
任务6执行完毕!
任务5执行完毕!
执行结果分析:
我们可以看到,在线程池中线程数量为10, 队列中等待的的任务数量为5时,新任务来了就抛出异常了。
上面的执行结果中,当等待队列满了,核心线程数也满了,还未达到最大线程数时,新任务到来会先于等待队列执行。
默认的拒绝策略是AbortPolicy,抛出异常并拒绝接受新任务。 拒绝接受任务的时机是:线程数量等于maximumPoolSize且阻塞队列已满时。
四种常见的线程池
ExecutorService是Java提供的用于管理线程池的类。该类的两个作用:控制线程数量和重用线程。
-
Executors.newCacheThreadPool()
缓存线程池,新来任务时,先看池子里是否有线程可以使用,如果有就使用池子里的,没有就新建线程到线程池中执行任务。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
通过构造方法可以看到,核心线程数为0,代表如果没有任务,线程池中线程池最终会变为0,最大线程数为Integer.MAX_VALUE,线程空闲存活时间为60秒,使用SynchronousQueue队列,新来的任务不会保存到队列中。
-
Executors.newFixedThreadPool(int nThreads)
固定线程数量的线程池,从构造方法可以看出线程池的线程数量固定为nThreads,没有空闲线程时任务进入阻塞队列等待。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
Executors.newScheduledThreadPool(int corePoolSize)
创建一个定长线程池,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
延迟执行schedule():
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.schedule(
() -> System.out.println("延迟2秒后输出"),
2,
TimeUnit.SECONDS
);
scheduledExecutorService.shutdown();
定期执行 scheduleAtFixedRate():
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleAtFixedRate(
()-> System.out.println("定期循环执行"),
0,2,TimeUnit.SECONDS);
这里还有一个方法是定期执行:scheduleWithFixedDelay();
它也是延期执行,但是它们有区别:
scheduleAtFixedRate()是按照固定时间去执行,在前一次任务执行完后如果执行任务时间大于延迟执行时间,则立刻执行下一次任务。
scheduleWithFixedDelay()是在上一次任务执行完后再延迟固定的时间再执行下一次任务。
示例:
scheduleAtFixedRate():
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleAtFixedRate(()-> {
try {
System.out.print("系统时间:"+ LocalDateTime.now().getMinute()+":"+LocalDateTime.now().getSecond());
System.out.println(" 延期执行下一任务");
Thread.sleep(6000);
System.out.println("睡眠6秒,延迟5秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
},0,5,TimeUnit.SECONDS);
执行结果:
可以看到上一次任务执行完后立刻执行下一次任务
scheduleWithFixedDelay():
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleWithFixedDelay(()-> {
try {
System.out.print("系统时间:"+ LocalDateTime.now().getMinute()+":"+LocalDateTime.now().getSecond());
System.out.println(" 延期执行下一任务");
Thread.sleep(6000);
System.out.println("睡眠6秒,延迟5秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
},0,5,TimeUnit.SECONDS);
执行结果:
可以看到在11秒后(上一任务完成后延迟5秒)才执行下一次任务。
- Executors.newSingleThreadExecutor()
单身狗线程池,只有一个线程,线程空闲即销毁,所有任务顺序执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
在使用newCacheThreadPool和newFixedThreadPool线程池的时候有OOM的风险,如果创建的每个线程都一直运行,那么新任务会不断创建线程,jdk8默认一个线程大小为1MB,线程多了就会造成OOM,建议根据实际情况设置线程池参数。
动态参数设置
JDK原生线程池ThreadPoolExecutor提供了如下几个public的setter方法,可以动态设置线程池的参数。
参考资料