Java线程池作为Java多线程中更高级的知识点,也是面试中常被问到的知识点,下面带着几个常见的面试问题剖析下Java多线程中的线程池。
1. 经典面试题
- 说说对Java线程池的理解,ThreadPoolExecutor各个参数的作用,如何进行的?
- 几种常见的线程池及使用场景?
- 线程池都有哪几种工作队列?
- 使用无界队列的线程池会导致内存飙升吗?
- 线程池中的线程抛异常会怎样?
2. 线程池的概念
- 一句话简单概括:管理线程的池子
- 主要优点:
- 统一管理线程,避免线程的重复创建和销毁(线程也是一个类,而创建一个类对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的)
- 重复利用。 线程使用直接从池子取,用完放回池子,可重复利用,节省资源。
- 提高响应速度。 相对于从线程池拿线程,重新创建一个线程执行速度会慢很多。
3. 线程池的创建方式
线程池可以通过ThreadPoolExecutor类来创建,看下他参数最长的那个构造器
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
分别看下几个参数的意义
- corePoolSize: 线程池核心线程数最大值
- maximumPoolSize: 线程池中允许存放的最大线程数(核心线程数+非核心线程数)
- keepAliveTime: 非核心线程池中空闲线程存活的时间
- unit: 线程空闲存活时间单位,即keepAliveTime的时间单位,纳秒、毫秒、秒等
- workQueue: 存放任务的阻塞队列,分为有界队列和无界队列,用于存放等待执行的任务,
- threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题,一般都是采用
Executors.defaultThreadFactory()方法返回的DefaultThreadFactory - handler: 线程池的饱和策略(拒绝策略)事件,主要有四种类型(AbortPolicy、CallerRunsPolicy、 DiscardPolicy、DiscardOldestPolicy)
4. 线程池任务的执行流程
- 通过execute()方法提交一个任务,当线程池里存活的核心线程数小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务
- 如果线程池中核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行
- 当线程池里存活的线程数已经等于corePoolSize,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
- 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用相应的拒绝策略进行处理
// 代码演示执行流程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i < 50; i++) {
int finalI = i;
threadPoolExecutor.execute(() -> {
try {
System.out.println("当前执行线程:" + finalI);
Thread.sleep(1000);
System.out.print("-------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
执行结果
当前执行线程:1
当前执行线程:4
当前执行线程:3
当前执行线程:2
当前执行线程:6
当前执行线程:5
当前执行线程:7
当前执行线程:8
当前执行线程:9
当前执行线程:10
当前执行线程:21
当前执行线程:22
当前执行线程:26
当前执行线程:24
当前执行线程:23
当前执行线程:25
当前执行线程:27
当前执行线程:28
当前执行线程:29
当前执行线程:30
Exception in thread "main" java.util.concurrent.RejectedExecutionException....
-----------当前执行线程:12
-----当前执行线程:20
当前执行线程:19
当前执行线程:18
当前执行线程:17
当前执行线程:16
当前执行线程:15
当前执行线程:14
当前执行线程:13
-------------当前执行线程:11
- 由代码演示可以看出,只有1-10和21-30先执行,中间的11-20过了1s后才执行,并且在执行到30之后会抛出RejectedExecutionException,这也验证了这一点,先创建1-10号核心线程,11-20加入到队列数最大为10的线程池阻塞队列,此时阻塞队列已满,21-30便加入非核心线程池,31之后的会执行拒绝策略(默认拒绝策略为抛异常),等到核心线程空闲便将阻塞队列中的任务开始执行
5. 线程池的工作队列
线程池的工作队列分为有界队列和无界队列,主要用于存放等待执行的任务
- 有界队列:使用有界队列,添加新的任务进来时,如果线程池实际线程数小于corePoolSize(核心线程数),则优先创建核心线程,如果线程池实际线程数大于corePoolSize(核心线程数),则会将任务加入队列,若队列已满,则在线程数不大于maximumPoolSize(最大线程数)的前提下,创建新的线程,若线程数大于maximumPoolSize(最大线程数),则执行拒绝策略。常见的有界队列有:
ArrayBlockingQueue - 无界队列:使用无界队列,maximumPoolSize(最大线程数)和拒绝策略均会失效,因为队列是没有限制的,所以就不存在队列满的情况。和有界队列相比,当有新的任务添加进来时,都会进入队列等待。但是这也会出现一些问题,例如
线程的执行速度比任务提交速度慢,会导致无界队列快速增长,内存不断飙升,直到系统资源耗尽。常见的无界队列有:PriorityBlockingQueue
5.1 常见的线程池工作队列
- ArrayBlockingQueue(
有界队列):是一个用数组实现的有界阻塞队列,按FIFO(先进先出)排序 - LinkedBlockingQueue(
可设置容量队列):基于链表结构的阻塞队列,按FIFO排序,容量可进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newFixedThreadPool线程池使用了这个队列 - DelayQueue(
延迟队列):将任务按周期延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列 - PriorityBlockingQueue(
优先级队列):具有优先级的无界阻塞队列 - SynchronousQueue(
同步队列):一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。
6. 线程池的拒绝策略
- AbortPolicy:抛出一个异常,默认的拒绝策略
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列里最老的任务,将当前这个任务继续提交给线程池
- CallerRunsPolicy:交给线程池调用所在的线程进行处理
- 自定义:可通过实现RejectedExecutionHandle接口来实现自定义拒绝策略
7. 常见线程池
- newFixedThreadPool:固定数目线程的线程池
- newCachedThreadPool:可缓存线程的线程池
- newSingleThreadExecutor:单线程的线程池
- newScheduledThreadPool:定时及周期执行的线程池
7.1 newFixedThreadPool
- 源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
- 特点:
- 核心线程数和最大线程数大小一样,可传参自定义
- 没有所谓的非空闲时间,即keepAliveTime为0
- 阻塞队列为无界队列LinkedBlockingQueue
- 工作机制:
- 通过execute提交任务
- 如果线程数少于核心线程,创建核心线程执行任务
- 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
- 如果线程执行完任务,去阻塞队列取任务,继续执行。
- 实例:
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
String currentTime= formatter.format(Calendar.getInstance().getTime());
System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
执行结果:
pool-1-thread-2执行,时间为:16:26:51
pool-1-thread-3执行,时间为:16:26:51
pool-1-thread-1执行,时间为:16:26:51
pool-1-thread-2执行,时间为:16:26:52
pool-1-thread-3执行,时间为:16:26:52
pool-1-thread-1执行,时间为:16:26:52
pool-1-thread-2执行,时间为:16:26:53
pool-1-thread-1执行,时间为:16:26:53
pool-1-thread-3执行,时间为:16:26:53
pool-1-thread-1执行,时间为:16:26:54
- 使用场景:处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能少的分配线程,即适用执行长期的任务(
由于使用了无界队列,当任务执行耗时长无法释放,而不断新增任务时会有内存飙升甚至OOM的风险)
7.2 newCachedThreadPool
- 源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
- 特点:
- 核心线程数可传参自定义
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列是SynchronousQueue
- 非核心线程空闲存活时间为60秒
- 工作机制:
- 通过execute提交任务
- 由于没有核心线程,所以任务会直接加到SynchronousQueue队列。
- 判断是否有空闲线程,如果有,就去取出任务执行。
- 如果没有空闲线程,就新建一个线程执行。
- 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。
- 实例:
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
String currentTime= formatter.format(Calendar.getInstance().getTime());
System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
执行结果:
pool-1-thread-9执行,时间为:16:25:55
pool-1-thread-2执行,时间为:16:25:55
pool-1-thread-3执行,时间为:16:25:55
pool-1-thread-8执行,时间为:16:25:55
pool-1-thread-5执行,时间为:16:25:55
pool-1-thread-7执行,时间为:16:25:55
pool-1-thread-6执行,时间为:16:25:55
pool-1-thread-4执行,时间为:16:25:55
pool-1-thread-10执行,时间为:16:25:55
pool-1-thread-1执行,时间为:16:25:55
- 使用场景:用于并发执行大量短期的小任务
7.3 newSingleThreadExecutor
- 源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
- 特点:
- 核心线程数与最大线程数均为1
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列是LinkedBlockingQueue
- 工作机制:
- 通过execute提交任务
- 线程池是否有一个线程,如果没有,新建线程执行任务
- 如果有,将任务加到LinkedBlockingQueue阻塞队列
- 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条线程)夜以继日地干活。
- 实例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
String currentTime= formatter.format(Calendar.getInstance().getTime());
System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
执行结果:
pool-1-thread-1执行,时间为:16:24:40
pool-1-thread-1执行,时间为:16:24:41
pool-1-thread-1执行,时间为:16:24:42
pool-1-thread-1执行,时间为:16:24:43
pool-1-thread-1执行,时间为:16:24:44
pool-1-thread-1执行,时间为:16:24:45
pool-1-thread-1执行,时间为:16:24:46
pool-1-thread-1执行,时间为:16:24:47
pool-1-thread-1执行,时间为:16:24:48
pool-1-thread-1执行,时间为:16:24:49
- 使用场景:任务串行执行,一个任务一个任务地顺序执行
7.4 newScheduledThreadPool
- 源码:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
- 特点:
- 核心线程数可传参自定义
- 最大线程数为Integer.MAX_VALUE
- keepAliveTime为0
- 阻塞队列为延迟队列DelayedWorkQueue
- 方法scheduleAtFixedRate()可让任务按某种速率周期执行
- 方法scheduleWithFixedDelay()可让任务在延迟后执行
- 工作机制:
- 通过execute提交任务
- 线程池中的线程从延迟队列DelayQueue中取任务
- 线程从 DelayQueue 中获取 time 大于等于当前时间的任务
- 执行完后修改这个任务task的 time 为下次被执行的时间
- 这个任务放回DelayQueue队列中
- 实例:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
scheduledExecutorService.scheduleWithFixedDelay(() -> {
SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
String currentTime = formatter.format(Calendar.getInstance().getTime());
System.out.println(Thread.currentThread().getName() + "执行,时间为:" + currentTime);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 3, TimeUnit.SECONDS);
执行结果:
pool-1-thread-1执行,时间为:17:34:42
pool-1-thread-1执行,时间为:17:34:46
pool-1-thread-1执行,时间为:17:34:50
pool-1-thread-1执行,时间为:17:34:54
pool-1-thread-1执行,时间为:17:34:58
pool-1-thread-1执行,时间为:17:35:02
pool-1-thread-1执行,时间为:17:35:06
...
- 使用场景:任务需要周期性的执行,需要限制线程数量
8. 线程池的运行状态
跟线程一样,线程池也存在自己的运行状态
// ThreadPoolExecutor.java
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;
- RUNNING
- 线程池会同时处理阻塞任务和接新任务
- 调用shutdown()进入SHUTDOWN状态、调用shutdownNow()进入STOP状态
- SHUTDOWN
- 线程池只会处理阻塞队列中的任务,不再接新任务
- 当处理完所有任务,此时任务队列也空了,会进入TIDYING状态
- STOP
- 线程池既不会接新任务,也不会处理阻塞队列中的任务,并且还会中断正在运行的任务
- 当任务队列为空会进入TIDYING状态
- TIDYING
- 线程池中所有的任务已经运行终止,记录的任务数量为0。
- 当terminated()执行完毕,标志着进入TERMINATED状态
- TERMINATED
- 线程池彻底终止
9. 线程池中的异常处理方式
线程池处理任务时,线程中可能会抛出RuntimeException异常,当然线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,而我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executorService.submit(() -> {
System.out.println("当前执行线程" + Thread.currentThread().getName());
Object object = null;
object.toString();
});
}
}
执行结果:
当前执行线程pool-1-thread-2
当前执行线程pool-1-thread-1
当前执行线程pool-1-thread-3
当前执行线程pool-1-thread-1
当前执行线程pool-1-thread-2
可见并未抛出异常的信息,那么如何感知异常处理异常呢,常见有以下几种方法
9.1 try-catch
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executorService.submit(() -> {
Object object = null;
try {
object.toString();
} catch (Exception e) {
System.out.println("当前执行线程:" + Thread.currentThread().getName() + " 抛出异常:" + e);
}
});
}
执行结果:
当前执行线程:pool-1-thread-2 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-3 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-1 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-2 抛出异常:java.lang.NullPointerException
当前执行线程:pool-1-thread-3 抛出异常:java.lang.NullPointerException
9.2 Future.get()
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
Future<?> future = executorService.submit(() -> {
System.out.print("当前执行线程" + Thread.currentThread().getName());
Object object = null;
object.toString();
});
try {
future.get();
} catch (Exception e) {
System.out.println(",抛出异常:" + e);
}
}
执行结果:
当前执行线程pool-1-thread-1,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-2,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-3,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-1,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
当前执行线程pool-1-thread-2,抛出异常:java.util.concurrent.ExecutionException: java.lang.NullPointerException
9.3 UncaughtExceptionHandler
可以通过线程设置UncaughtExceptionHandler来达到监听的目的
ExecutorService executorService = Executors.newFixedThreadPool(3, thread -> {
Thread thread1 = new Thread(thread);
thread1.setUncaughtExceptionHandler((t1, e) -> {
System.out.println("当前执行线程" + t1.getName() + "抛出异常:" + e);
});
return thread1;
});
for (int i = 0; i < 5; i++) {
executorService.execute(() -> {
Object object = null;
object.toString();
});
}
执行结果:
当前执行线程Thread-2抛出异常:java.lang.NullPointerException
当前执行线程Thread-4抛出异常:java.lang.NullPointerException
当前执行线程Thread-0抛出异常:java.lang.NullPointerException
当前执行线程Thread-1抛出异常:java.lang.NullPointerException
当前执行线程Thread-5抛出异常:java.lang.NullPointerException