浅谈线程池
1. 简介
在互联网的开发场景下,很多业务场景下我们需要使用到多线程的技术,从 Java 5 开始,Java 提供了自己的线程池,线程池就是一个线程的容器,每次只执行额定数量的线程。java.util.concurrent包中提供了ThreadPoolExecutor类来管理线程,本文将介绍一下ThreadPoolExecutor类的使用。
2.为什么要使用线程池?
在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
- 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。
3.线程池使用方式
java.util.concurrent包中提供了多种线程池的创建方式,我们可以直接使用ThreadPoolExecutor类直接创建一个线程池,也可以使用Executors类创建,下面我们分别说一下这几种创建的方式。
4.Executors创建线程池
Executors类是java.util.concurrent提供的一个创建线程池的工厂类,使用该类可以方便的创建线程池,此类提供的几种方法,支持创建四种类型的线程池,分别是:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。
4.1 newCachedThreadPool
创建一个可缓存的无界线程池,该方法无参数。当线程池中的线程空闲时间超过60s则会自动回收该线程,当任务超过线程池的线程数则创建新线程。线程池的大小上限为Integer.MAX_VALUE,可看做是无限大。
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) { // 循环第二次 闲置60s, 复用上一个任务
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("当前线程,执行耗时任务,线程是:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
上面的创建了一个无边界限制的线程池,下面可以看到执行结果。在线程池无限大的情况下,当前可以运行的线程数量就不会收到限制,也就是创建多少线程即执行多少,下面的结果中有显示15条线程
当前线程,执行耗时任务,线程是:pool-1-thread-2
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-3
当前线程,执行耗时任务,线程是:pool-1-thread-13
当前线程,执行耗时任务,线程是:pool-1-thread-6
当前线程,执行耗时任务,线程是:pool-1-thread-10
当前线程,执行耗时任务,线程是:pool-1-thread-12
当前线程,执行耗时任务,线程是:pool-1-thread-11
当前线程,执行耗时任务,线程是:pool-1-thread-9
当前线程,执行耗时任务,线程是:pool-1-thread-4
当前线程,执行耗时任务,线程是:pool-1-thread-5
当前线程,执行耗时任务,线程是:pool-1-thread-7
当前线程,执行耗时任务,线程是:pool-1-thread-8
当前线程,执行耗时任务,线程是:pool-1-thread-20
当前线程,执行耗时任务,线程是:pool-1-thread-18
当前线程,执行耗时任务,线程是:pool-1-thread-17
当前线程,执行耗时任务,线程是:pool-1-thread-16
当前线程,执行耗时任务,线程是:pool-1-thread-19
当前线程,执行耗时任务,线程是:pool-1-thread-15
当前线程,执行耗时任务,线程是:pool-1-thread-14
4.2 newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) { // 循环第二次 闲置60s, 复用上一个任务
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("当前线程,执行耗时任务,线程是:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
下面可以看到结果:一直限制在指定线程数量范围内,最大创建线程数5个
当前线程,执行耗时任务,线程是:pool-1-thread-5
当前线程,执行耗时任务,线程是:pool-1-thread-4
当前线程,执行耗时任务,线程是:pool-1-thread-3
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-2
当前线程,执行耗时任务,线程是:pool-1-thread-4
当前线程,执行耗时任务,线程是:pool-1-thread-3
当前线程,执行耗时任务,线程是:pool-1-thread-5
当前线程,执行耗时任务,线程是:pool-1-thread-2
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-4
当前线程,执行耗时任务,线程是:pool-1-thread-2
当前线程,执行耗时任务,线程是:pool-1-thread-3
当前线程,执行耗时任务,线程是:pool-1-thread-5
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-2
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-5
当前线程,执行耗时任务,线程是:pool-1-thread-3
当前线程,执行耗时任务,线程是:pool-1-thread-4
4.3 newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
/**
* 创建给定延迟后运行命令或者定期地执行的线程池
*/
public static void createScheduledThreadPool() {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
final CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int currentIndex = i;
//定时执行一次的任务,延迟1s后执行
scheduledThreadPool.schedule(() -> {
System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
countDownLatch.countDown();
}, 1, TimeUnit.SECONDS);
//周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
scheduledThreadPool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "every 3s"), 2, 3, TimeUnit.SECONDS);
}
}
这里创建了一个调度的线程池,执行两个任务,第一个任务延迟1秒后执行,第二个任务为周期性任务,延迟2秒后,每三秒执行一次
pool-1-thread-1, currentIndex is : 0
pool-1-thread-2, currentIndex is : 1
pool-1-thread-3, currentIndex is : 2
pool-1-thread-2, currentIndex is : 3
pool-1-thread-4, currentIndex is : 4
pool-1-thread-5every 3s
pool-1-thread-2every 3s
pool-1-thread-3every 3s
pool-1-thread-1every 3s
pool-1-thread-5every 3s
pool-1-thread-2every 3s
pool-1-thread-4every 3s
pool-1-thread-4every 3s
pool-1-thread-3every 3s
pool-1-thread-2every 3s
可以看到,第一个任务执行完毕后,开始执行定时调度型任务
该线程池提供了多个方法:
- schedule(Runnable command, long delay, TimeUnit unit),延迟一定时间后执行Runnable任务;
- schedule(Callable callable, long delay, TimeUnit unit),延迟一定时间后执行Callable任务;
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit),延迟一定时间后,以间隔period时间的频率周期性地执行任务;
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit),与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。
4.4 newSingleThreadExecutor
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 20; i++) { // 循环第二次 闲置60s, 复用上一个任务
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("当前线程,执行耗时任务,线程是:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
可以看到执行结果:当前只能存在一个线程任务
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-1
当前线程,执行耗时任务,线程是:pool-1-thread-1
。
。
上面只会复用线程thread-1
5. 四种线程池对比
线程池方法 | 初始化线程池数 | 最大线程池数 | 时间单位 | 工作队列 |
---|---|---|---|---|
newCachedThreadPool | 0 | Integer.MAX_VALUE | 60秒 | SynchronousQueue |
newFixedThreadPool | 入参指定大小 | 入参指定大小 | 0毫秒 | LinkedBlockingQueue |
newScheduledThreadPool | 入参指定大小 | Integer.MAX_VALUE | 0微秒 | DelayedWorkQueue |
newSingleThreadExecutor | 1 | 1 | 0毫秒 | LinkedBlockingQueue |
6.ThreadPoolExecutor创建线程池
ThreadPoolExecutor executorService =
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
for (int i = 0; i < 20; i++) { // 循环第二次 闲置60s, 复用上一个任务
executorService.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("当前线程,执行耗时任务,线程是:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
各个参数含义:
- corePoolSize - 池中所保存的线程数,包括空闲线程,必须大于或等于0。
- maximumPoolSize - 池中允许的最大线程数,必须大于或等于corePoolSize。
- keepAliveTime - 线程存活时间,当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
- unit - keepAliveTime 参数的时间单位,必须大于或等于0。
- workQueue - 工作队列,执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
- threadFactory - 执行程序创建新线程时使用的工厂,默认为DefaultThreadFactory类。
- handler - 拒绝策略,由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序,默认策略为ThreadPoolExecutor.AbortPolicy。
在上述介绍的几种 Executors线程池 查看源码最终都是采用ThreadPoolExecutor
指定线程池大小
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
参数三/四:时间数值keepAliveTime, 单位:时分秒 60s
只有在正在执行的任务Runnable > corePoolSize --> 参数三/参数四 才会起作用
Runnable1执行完毕后 闲置60s,如果过了闲置60s,会回收掉Runnable1任务,,如果在闲置时间60s 复用此线程Runnable
workQueue队列 :会把超出的任务加入到队列中 缓存起来