线程频繁的创建和销毁会带来太多的调度开销,为了减少这种开销可以使用线程池技术。线程池内部能够维护多个线程随时分配,相比频繁创建销毁线程,使用线程池能够降低资源消耗、提高响应速度,此外还能够对线程数进行统一控制,避免耗尽系统资源。
这里我们先介绍一些线程池经常用到的概念。
corePoolSize:线程池的核心线程数
maximumPoolSize:线程池能容纳的最大线程数
keepAliveTime:空闲线程的存活时间
workQueue:存放提交但未执行的阻塞队列
threadFactory:创建线程的工厂类
handler:队列满后的拒绝策略
我们向线程池提交任务,当线程数小于corePoolSize
时,线程池会立即创建线程;当提交任务数大于corePoolSize
时,会优先将任务存放到workQueue
;当队列饱和后会扩充线程池的线程数直到达到maximumPoolSize
,此时再有多余的任务就会触发线程池的拒绝策略;当线程无事可做超过了keepAliveTime
时,线程池的线程数就会逐步收缩直到达到corePoolSize
。
这里注意阻塞队列如果使用ArrayBlockingQueue
是需要指定队列初始值的,但如果使用LinkedBlockingQueue
且没有指定队列容量那么默认就没有最大值了(即最大值为Integer.MAX_VALUE
),然而这可能导致队列任务积压过多,实际开发中十分不建议不指定队列容量。
对于拒绝策略,则有以下几种:
AbortPolicy:触发拒绝策略时,直接抛出RejectedExecutionException
异常拒绝执行任务,这也是线程池默认的拒绝策略。
CallerRunsPolicy:触发拒绝策略时,只要线程池还没关闭就使用调用线程直接运行。这种策略适合并发量小、不允许失败的场景,如果并发量较大会导致主线程严重阻塞,性能损失较大。
DiscardPolicy:直接丢弃任务,没有其它操作。
DiscardOldestPolicy:触发拒绝策略时,丢弃阻塞队列中最老的任务,将新任务加入。
这里我们直接看一个使用线程池的例子。
package com.gacfox.demo;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = new ThreadPoolExecutor(
3,
3,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory()
);
// 使用线程池调度10个任务
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
try {
Thread.sleep(1000);
System.out.println("线程" + Thread.currentThread().getId() + "执行完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
}
代码中,我们创建了一个大小为3的线程池,使用容量为10的ArrayBlockingQueue
作为任务队列,之后我们用这个线程池调度10个任务。
注:上面代码线程池没有关闭,因此程序运行完成后也不会退出。实际开发中,如果我们需要关闭线程池,可以调用线程池的shutdown()
方法。
JDK中的Excutors
为我们提供了几种默认的线程池实现。
线程池 | 说明 |
---|---|
newCachedThreadPool | 可缓存线程池,当线程池中线程超过空闲时间时回收,当任务需要线程超过线程池线程数时会新建线程 |
newFixedThreadPool | 固定大小线程池,任务需要的线程超出线程池大小时会阻塞等待 |
newScheduleThreadPool | 固定大小线程池,支持定时和周期性执行任务 |
newSingleThreadExecutor | 单线程化的线程池,它能保证所有任务按照FIFO、LIFO或优先级顺序执行 |
下面是一个例子。
package com.gacfox.demo;
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 使用线程池调度10个任务
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
try {
Thread.sleep(1000);
System.out.println("线程" + Thread.currentThread().getId() + "执行完毕");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
}
}
代码中,我们直接用Executors
创建了一个默认的定长线程池,并用其调度10个任务。
不过实际使用中,我们发现这些JDK默认的线程池实现都没有队列容量参数,前面说过指定一个合适的队列容量能够避免系统任务积压,因此这个参数还是比较重要的,JDK实现的这几个线程池却无法设置该参数,这就比较尴尬了,因此不建议使用这些线程池实现,还是建议使用new ThreadPoolExecutor()
的方式创建线程池。