一、线程池
1. 线程池使用
1.1 如何配置线程池大小
如何配置线程池大小要看业务系统执行的任务更多的是计算密集型任务,还是I/O密集型任务。大家可以从这两个方面来回答面试官。
(1)如果是计算密集型任务,通常情况下,CPU个数为N,设置N + 1个线程数量能够实现最优的资源利用率。因为N + 1个线程能保证至少有N个线程在利用CPU,提高了CPU利用率;同时不设置过多的线程也能减少线程状态切换所带来的上下文切换消耗。
(2)如果是I/O密集型任务,线程的主要等待时间是花在等待I/O操作上,另外就是计算所花费的时间。一般可以根据这个公式得出线程池合适的大小配置。 $$ 线程池大小 = CPU数量 * CPU期望的利用率 * (1 + IO操作等待时间/CPU计算时间) $$
1.2 创建线程池
我们可以使用ThreadPoolExecutor自定义创建线程池,这也是创建线程池推荐的创建方式。
public ThreadPoolExecutor(int corePoolSize, // 要保留在池中的线程数
int maximumPoolSize, // 池中允许的最大线程数
long keepAliveTime, // 当线程数大于corePoolSize时,多余的空闲线程在终止之前等待新任务的最长时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 在执行任务之前用于保存任务的队列
ThreadFactory threadFactory) { // 执行程序创建新线程时使用的工厂
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
另外Executors类也提供了一些静态工厂方法,可以用来创建一些预配置的线程池。
newFixedThreadPool可以设置线程池的固定线程数量。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newSingleThreadExecutor可以让线程按序执行,适用于需要确保所有任务按序执行的场景。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
大家看下以下源码,newCachedThreadPool的线程数没有上限限制,同时空闲线程的存活时间是60秒。newCachedThreadPool更适合系统负载不太高、线程执行时间短的场景下,因为线程任务不需要经过排队,直接交给空闲线程就可以。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newScheduledThreadPool可以安排任务在给定的延迟后运行,或者定期执行。
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
1.3 预配置线程池弊端
小伙伴要记得上述静态工厂方法在使用过程中可能会出现OOM内存溢出的情况。
newFixedThreadPool
、newSingleThreadExecutor
:因为线程池指定的请求队列类型是链表队列LinkedBlockingQueue<Runnable>()
,故允许的请求队列长度是无上限的,可能会出现OOM内存溢出。newCachedThreadPool
、newScheduledThreadPool
:线程池指定的线程数上限是Integer.MAX_VALUE,故允许创建的线程数量是无上限的Integer.MAX_VALUE,可能会出现OOM内存溢出。
1.3 Spring创建线程池
一般Spring工程创建线程池不直接使用ThreadPoolExecutor。
Spring框架提供了以Bean形式来配置线程池的ThreadPoolTaskExecutor
类,ThreadPoolTaskExecutor类的底层实现还是基于JDK的ThreadPoolExecutor。
# 示例代码
@Bean(name = "testExecutor")
public ThreadPoolTaskExecutor testExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 配置核心线程数
executor.setCorePoolSize();
// 配置最大线程数
executor.setMaxPoolSize();
// 配置队列大小
executor.setQueueCapacity();
executor.initialize();
return executor;
}
2. 线程池拒绝策略
大家如果有看ThreadPoolExecutor源码就知道,ThreadPoolExecutor类实现了setRejectedExecutionHandler
方法,顾名思义意思是设置拒绝执行处理程序。
# ThreadPoolExecutor源码
/**
* Sets a new handler for unexecutable tasks. // 为无法执行的任务设置新的处理程序
*
* @param handler the new handler
* @throws NullPointerException if handler is null
* @see #getRejectedExecutionHandler
*/
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
if (handler == null)
throw new NullPointerException();
this.handler = handler;
}
该方法可以为线程池设置拒绝策略,目前JDK8一共有四种拒绝策略,也对应入参RejectedExecutionHandler的四种子类实现。
- AbortPolicy:默认的拒绝策略,直接抛出RejectedExecutionException异常。
- CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
- DiscardPolicy:直接丢弃被拒绝的任务。
- DiscardOldestPolicy:丢弃最旧的未处理请求,然后重试execute 。
另外如果线程池拒绝策略设置为DiscardOldestPolicy,线程池的请求队列类型最好不要设置为优先级队列PriorityBlockingQueue。因为该拒绝策略是丢弃最旧的请求,也就意味着丢弃优先级最高的请求。
3. 线程工厂的作用
ThreadFactory定义了创建线程的工厂,回答这个问题我们就要结合实际场景了。
ThreadFactory线程工厂能够为线程池里每个线程设置名称、同时设置自定义异常的处理逻辑,可以方便我们通过日志来定位bug的位置。
以下是一个代码示例。
@Slf4j
public class CustomGlobalException {
public static void main(String[] args) {
ThreadFactory factory = r -> {
String threadName = "线程A";
Thread thread = new Thread(r, threadName);
thread.setUncaughtExceptionHandler((t, e) -> {
log.error("{}执行了自定义异常日志", threadName);
});
return thread;
};
ExecutorService executor = new ThreadPoolExecutor(6,
6,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(66),
factory);
executor.execute(() -> {
throw new NullPointerException();
});
executor.shutdown();
}
}
控制台打印:2024-04-26 22:04:45[ ERROR ]线程A执行了自定义异常日志
2. 线程通信
2.1 线程的等待/通知机制
Java线程的等待/通知机制指的是:线程A获得了synchronized同步方法、同步方法块的锁资源后,调用了锁对象的wait()方法,释放锁的同时进入等待状态;而线程B获得锁资源后,再通过锁对象的notify()或notifyAll()方法来通知线程A恢复执行逻辑。
其实Java的所有对象都拥有等待/通知机制的本领,大家可以在JDK源码package java.lang`下找到Java.lang.Object里提供的五个与等待/通知机制相关的方法。
一、等待。
(1)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。
public final void wait() throws InterruptedException {
wait(0);
(2)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的毫秒timeout过去。
public final native void wait(long timeout) throws InterruptedException;
(3)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的毫秒timeout过去,另外nanos是额外时间,以纳秒为单位。
public final void wait(long timeout, int nanos) throws InterruptedException {
}
所以其实wait()、watit(0)、watit(0, 0)执行后都是同样的效果。
二、通知。
(1)唤醒在此对象监视器上等待的单个线程。
public final native void notify();
(2)唤醒在此对象监视器上等待的所有线程。
public final native void notifyAll();
大家有没听说过消费者生产者问题呢?消费者生产者之间要无限循环生产和消费物品,解决之道就是两者形成完美的等待、通知机制。而这套机制就可以通过上文的wait、notify方法来实现。
2.2 线程通信方式
(1)利用Condition进行线程通信。
如果大家的程序直接采用的是Lock对象来同步,则没有了上文synchronized锁带来的隐式同步器,也就无法使用wait()、notify()方法。
此时的线程可以使用Condition对象来进行通信。例如下文的示例代码: condition0的await()阻塞当前线程,同时释放、等待获取锁资源;接着等待其他线程调用condition0的signal()来通知其获取锁资源继续执行。
@Slf4j
public class UseReentrantLock {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition0 = lock.newCondition();
private static final Condition condition1 = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
for (int i = 1; i < 4; i++) {
log.info(i + "");
condition1.signal();
condition0.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
for (int i = 65; i < 68; i++) {
log.info((char) i + "");
condition0.signal();
condition1.await();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
}
# 程序执行结果
2024-05-30 10:30:30[ INFO ]1
2024-05-30 10:30:30[ INFO ]A
2024-05-30 10:30:30[ INFO ]2
2024-05-30 10:30:30[ INFO ]B
2024-05-30 10:30:30[ INFO ]3
2024-05-30 10:30:30[ INFO ]C
(2)Thread采用join方法进行通信。
线程Thread对象还提供了join方法,也是一种通信的方式。当某个程序的执行流调用了某个thread对象的join方法,调用线程将会被阻塞,等到thread对象终止后才通知调用线程继续执行。
public final void join() throws InterruptedException {
join(0);
}
(3)volatile共享变量。
volatile的出现,大家是不是有些意外呢?虽然volatile适用的多线程场景不多,但它也是线程通信的一种方式。被volatile修饰的变量如果更新了值,则会通过主内存这条消息总线通知所有使用该变量的线程,让其把主内存同步到工作内存里,则所有线程都会获取共享变量最新值。
2.3 更加灵活的ReentrantLock
在线程同步上,JDK的Lock接口提供了多个实现子类,如下所示。下面我按面试官面试频率高的ReentrantLock来讲解。
ReentrantLock相比synchronized来说使用锁更加灵活,可以自由进行加锁、释放锁。ReentrantLock类提供了lock()、unlock()来实现以上操作。具体实操代码可以看上一个面试官问题关于Condition的示例代码。
// ReentrantLock源码
package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
// 获取锁
public void lock() {
sync.lock();
}
// 尝试释放此锁
public void unlock() {
sync.release(1);
}
}
另外ReentrantLock和synchronized都是可重入锁,即线程获取锁资源后,下一步如果进入相同锁资源的同步代码块,不需要再获取锁。
ReentrantLock也可以实现公平锁,即成功获取锁的顺序与申请锁资源的顺序一致。我们在创建对象时进行初始化设置就可以设置为公平锁。
ReentrantLock lock = new ReentrantLock(true);
3. ThreadLocal作用
上文我们讨论的都是在多个线程对共享资源进行通信的业务场景上,例如商城业务秒杀的库存要保证数据安全性。而如果在多个线程对共享资源进行线程隔离的业务场景上,则可以使用ThreadLoccal来解决。
ThreadLocal可以保存当前线程的副本值,提供了set、get方法,通过set方法可以把指定值设置到当前线程副本;而通过get方法可以返回此当前线程副本中的值。
例如要实现一个功能,每个线程打印当前局部变量:局部变量 + 10
,我们就可以利用ThreadLocal保存共享变量i,来避免对变量i的共享冲突。
public class UseThreadLocal {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(20);
for (int i = 0; i < 3; i++) {
int number = i;
es.execute(() -> System.out.println(number + ":" + new intUtil().addTen(number)));
}
}
private static class intUtil {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); // 使用threadLocal保存线程保存的当前共享变量num
public static int addTen(int number) {
threadLocal.set(number);
try { // 休息1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return threadLocal.get() + 10;
}
}
}
# 程序执行结果
0:10
2:12
1:11
4. 线程生命周期
线程生命周期可以大致分为六步,如下所示。
-
初始状态。创建了线程对象还没有调用start()。
-
就绪或运行状态。执行了start()可能运行,也可能进入就绪状态在等待CPU资源。
-
阻塞状态 。一直没有获得锁。
-
等待状态。等待其他线程的通知唤醒。
-
超时状态。
-
终止状态。
二、并发编程面试题
1. 什么是线程池?为什么要使用线程池?
回答:
线程池是一种复用已创建线程的技术,通过提前创建一组线程并将它们存储在池中,可以在需要执行任务时直接从池中获取线程,而不需要每次都创建新的线程。这种机制可以减少线程的创建和销毁带来的性能开销,提高系统响应速度,并且通过合理配置线程池的大小,可以有效控制并发数量,避免系统资源耗尽。
使用线程池有以下几个主要优点:
- 降低资源消耗: 通过重复利用线程池中的线程,减少了频繁创建和销毁线程的开销。
- 提高响应速度: 线程池可以在接收到任务时直接使用现有线程,减少了创建线程的时间。
- 提高线程管理能力: 线程池可以根据系统的负载自动调节线程的数量,避免过多线程带来的资源争夺。
- 增强系统稳定性: 通过控制最大并发线程数,避免因创建过多线程导致系统崩溃。
2. Java中如何创建一个线程池?简述常用的ThreadPoolExecutor
构造方法。
回答:
Java中可以通过 ThreadPoolExecutor
来创建一个自定义的线程池。ThreadPoolExecutor
的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
各参数含义:
corePoolSize
: 核心线程数,表示线程池中始终保持存活的线程数,即使它们处于空闲状态。maximumPoolSize
: 最大线程数,表示线程池中允许的最大线程数。keepAliveTime
: 非核心线程空闲时的存活时间。当线程数超过corePoolSize
时,多余的空闲线程会在等待新任务的时间超过keepAliveTime
后被终止。unit
:keepAliveTime
的时间单位。workQueue
: 任务队列,用于存储等待执行的任务。threadFactory
: 线程工厂,用于创建新线程。handler
: 拒绝策略,在线程池饱和时用于处理无法执行的任务。
3. Java提供了哪些内置的线程池实现?它们的适用场景分别是什么?
回答:
Java 提供了以下几种常用的线程池实现:
newFixedThreadPool(int nThreads)
: 创建一个固定大小的线程池,核心线程数和最大线程数相同,适用于需要控制并发数量的场景,比如处理固定数量的任务。newCachedThreadPool()
: 创建一个缓存线程池,可以根据需要自动调整线程池大小,适用于执行大量短期任务的场景。newSingleThreadExecutor()
: 创建一个单线程的线程池,适用于需要保证任务顺序执行的场景。newScheduledThreadPool(int corePoolSize)
: 创建一个定时调度的线程池,适用于需要定时或周期性执行任务的场景。
4. newFixedThreadPool
和 newCachedThreadPool
的区别是什么?它们适用于哪些场景?
回答:newFixedThreadPool
和 newCachedThreadPool
是两种不同的线程池实现,主要区别如下:
-
newFixedThreadPool
:- 固定大小: 线程池大小固定,即核心线程数和最大线程数相同,且线程池中线程不会超出指定数量。
- 任务队列: 使用无界的
LinkedBlockingQueue
存储待执行的任务。 - 适用场景: 适合处理稳定、长期的任务,或者需要限制并发线程数量的场景,如对外部资源(数据库、文件等)进行有限制访问的场景。
-
newCachedThreadPool
:- 可扩展大小: 线程池大小动态调整,线程池中线程数量可以根据需要不断增加,空闲线程在 60 秒后会被回收。
- 任务处理: 当线程池中有空闲线程时会复用,若没有空闲线程则会创建新线程。
- 适用场景: 适合处理大量、短期的任务,或任务执行时间较短且不频繁的场景,如处理高并发请求。
5. ThreadPoolExecutor
的核心参数有哪些?分别起到什么作用?
回答:ThreadPoolExecutor
的核心参数有以下几个:
corePoolSize
: 核心线程数,即线程池中始终保持存活的线程数。maximumPoolSize
: 最大线程数,即线程池中允许的最大线程数。keepAliveTime
: 空闲线程存活时间,当线程数超过corePoolSize
时,多余的空闲线程会在等待新任务的时间超过keepAliveTime
后被终止。unit
:keepAliveTime
的时间单位。workQueue
: 任务队列,用于存储等待执行的任务。常见的有LinkedBlockingQueue
、SynchronousQueue
和ArrayBlockingQueue
。threadFactory
: 线程工厂,用于创建新线程,通常用于给线程设置一些属性,如名称、优先级等。handler
: 拒绝策略,在线程池及其任务队列都满时用于处理无法执行的任务。常见的策略有AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
和DiscardOldestPolicy
。
6. 如何设置线程池的核心线程数和最大线程数?如何判断合适的线程数?
回答:
线程池的核心线程数和最大线程数的设置取决于任务的类型和系统的硬件资源。一般来说,核心线程数 corePoolSize
应该设置为能够同时处理的并发任务数量,而最大线程数 maximumPoolSize
则设置为系统能够承受的最大并发任务数量。
常见的设置方法如下:
- CPU密集型任务: 对于CPU密集型任务(如计算、数据处理),通常建议设置核心线程数为CPU核心数+1,最大线程数可以略高于核心数。公式:
corePoolSize = CPU核心数 + 1
。 - I/O密集型任务: 对于I/O密集型任务(如文件读写、网络通信),由于线程在等待I/O操作完成时会处于空闲状态,因此可以设置更多的线程来提高并发度。公式:
corePoolSize = CPU核心数 * 2
或更多。
在设置时,建议通过性能测试来验证具体的设置是否合适,并根据实际情况进行调整。
7. 什么是线程池的拒绝策略?Java中有哪些内置的拒绝策略?
回答:
线程池的拒绝策略是在线程池和任务队列都满时,无法处理新提交任务时采取的处理措施。Java中的ThreadPoolExecutor
提供了几种内置的拒绝策略:
AbortPolicy
: 默认拒绝策略,直接抛出RejectedExecutionException
异常,阻止任务继续执行。CallerRunsPolicy
: 调用者运行策略,将任务回退到调用者线程执行,以降低提交新任务的速度。DiscardPolicy
: 丢弃策略,直接丢弃无法处理的新任务,不做任何处理也不抛出异常。DiscardOldestPolicy
: 丢弃最旧策略,丢弃任务队列中最旧的任务,然后尝试重新提交新任务。
8. 当任务提交速率超过线程池处理能力时,线程池会发生什么?如何处理这种情况?
回答:
当任务提交速率超过线程池处理能力时,线程池的任务队列会逐渐堆积,最终达到最大容量。这时,线程池将触发拒绝策略来处理无法执行的新任务。
处理这种情况的方法包括:
- 增加线程池大小: 增加
maximumPoolSize
,允许更多线程并发执行任务,缓解任务堆积的问题。 - 使用无界队列: 使用无界队列(如
LinkedBlockingQueue
),允许任务无限堆积(注意,这可能会导致内存溢出)。 - 优化任务执行时间: 优化任务代码,减少每个任务的执行时间,从而提升线程池处理速度。
- 使用拒绝策略: 使用合适的拒绝策略,如
CallerRunsPolicy
,让调用者线程执行任务,防止任务队列继续堆积。
9. 什么是 CallerRunsPolicy
拒绝策略?它在什么情况下适用?
回答:CallerRunsPolicy
是一种线程池的拒绝策略,当线程池和任务队列都满时,线程池不会抛出异常或丢弃任务,而是将任务回退给调用者线程执行。这种策略通过减缓新任务的提交速度来防止线程池任务堆积过多。
CallerRunsPolicy
适用于以下情况:
- 流量控制: 当系统流量突然增加时,通过让调用者线程执行任务,减缓任务提交速度,避免线程池超载。
- 任务必须执行: 在某些情况下,任务不能被丢弃或忽略,因此可以使用
CallerRunsPolicy
确保每个任务都能被执行。
10. 如何自定义线程池的拒绝策略?在什么情况下需要自定义?
回答:
自定义线程池的拒绝策略可以通过实现 RejectedExecutionHandler
接口来完成。具体步骤如下:
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义拒绝处理逻辑
// 比如记录日志、通知运维、重试任务等
}
}
然后在创建 ThreadPoolExecutor
时将其作为参数传入:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
new CustomRejectedExecutionHandler()
);
自定义拒绝策略适用于以下情况:
- 特殊业务需求: 当系统有特殊的业务需求,必须对拒绝的任务进行特定处理时,可以自定义策略。
- 复杂流量控制: 如果内置的拒绝策略不能满足系统的流量控制需求,可以通过自定义策略实现更复杂的流量控制逻辑。
11. 线程池中的任务队列是如何工作的?SynchronousQueue
和 LinkedBlockingQueue
有什么区别?
回答:
线程池中的任务队列用于存储等待执行的任务,常见的任务队列有:
-
SynchronousQueue
: 是一个无容量的队列,每个插入操作必须等待相应的移除操作。这意味着提交的任务不会被实际存储,而是直接传递给线程执行。如果没有可用线程,任务会被拒绝。因此,SynchronousQueue
适用于任务执行时间短且系统负载高的场景,能够快速地将任务交给空闲线程执行。 -
LinkedBlockingQueue
: 是一个基于链表的无界队列,可以存储大量待执行的任务。如果线程池中的线程全部繁忙,新的任务会被存放在队列中等待执行。LinkedBlockingQueue
适用于任务提交速率较高但执行时间较长的场景,能够有效缓解线程池压力。
两者的区别在于:
- 容量:
SynchronousQueue
没有容量,而LinkedBlockingQueue
是一个有容量限制的队列,默认是无界的。 - 适用场景:
SynchronousQueue
适合于任务较快、需要高吞吐量的场景;LinkedBlockingQueue
适合于任务执行时间长、需要排队的场景。
12. 线程池中的线程是如何被复用的?为什么线程池能够提高性能?
回答:
线程池通过复用线程来提高性能。当线程池接收到新任务时,会尝试使用池中已有的空闲线程来执行任务,而不是每次都创建新的线程。线程在执行完任务后,不会被销毁,而是重新回到池中,等待执行下一个任务。
线程池能够提高性能的原因有:
- 减少线程创建销毁的开销: 线程的创建和销毁是有成本的,频繁的创建和销毁会浪费CPU资源。通过复用线程,可以减少这些开销。
- 控制并发数量: 线程池可以限制同时执行的线程数量,避免系统因过多线程而资源耗尽或陷入死锁。
- 提高系统稳定性: 通过合理的线程管理,线程池能够确保系统在高负载下仍能稳定运行,避免因资源耗尽导致的系统崩溃。
13. ThreadPoolExecutor
中 keepAliveTime
参数的作用是什么?如何调整这个参数?
回答:keepAliveTime
参数定义了线程池中的非核心线程在没有任务执行时等待新任务的最长时间。如果超过这个时间且没有新任务到来,线程会被终止。这有助于减少线程池中不必要的线程资源占用。
调整 keepAliveTime
参数的建议:
- 短期任务: 如果任务执行时间短且频繁,可以将
keepAliveTime
设置得较短,以便快速回收空闲线程。 - 长时间运行的任务: 如果任务执行时间长或任务到来的间隔时间较长,可以适当增加
keepAliveTime
,避免频繁创建和销毁线程。
可以通过 ThreadPoolExecutor
的 setKeepAliveTime
方法动态调整这个参数:
executor.setKeepAliveTime(10, TimeUnit.SECONDS);
14. 如何优雅地关闭线程池?shutdown
和 shutdownNow
方法的区别是什么?
回答:
要优雅地关闭线程池,可以使用以下方法:
-
shutdown()
: 发起线程池的平滑关闭操作。此方法会停止接收新任务,并等待已提交的任务执行完毕。shutdown()
方法不会强制终止正在执行的任务,而是让它们执行完后关闭线程池。 -
shutdownNow()
: 立即关闭线程池。此方法会尝试取消所有正在执行的任务,并返回正在等待执行的任务列表。shutdownNow()
会强制终止所有活动的任务,但可能无法保证所有任务都被成功终止。
区别在于:
shutdown
: 平滑关闭,等待任务执行完毕,适用于大多数情况下的正常关闭。shutdownNow
: 强制关闭,适用于紧急情况或不再需要完成正在执行的任务时。
15. 如果在 ThreadPoolExecutor
中提交了一个任务,但任务抛出异常,会发生什么?如何捕获和处理这些异常?
回答:
在 ThreadPoolExecutor
中,提交的任务如果抛出异常,该异常会被 ThreadPoolExecutor
捕获并直接抛弃,不会影响其他任务的执行。默认情况下,这些异常不会被重新抛出,也不会被日志记录。
处理这些异常的方式包括:
- 在任务内部捕获异常: 可以在提交的
Runnable
或Callable
任务内部用try-catch
块捕获并处理异常。
executor.submit(() -> {
try {
// 任务逻辑
} catch (Exception e) {
// 异常处理
}
});
- 自定义
ThreadFactory
: 可以创建一个自定义的ThreadFactory
,为线程设置UncaughtExceptionHandler
,以便捕获线程中的未处理异常。
ThreadFactory threadFactory = runnable -> {
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler((t, e) -> {
// 处理未捕获的异常
});
return thread;
};
- 使用
Future
获取异常: 如果任务是通过submit
提交的,可以通过返回的Future
获取任务执行过程中抛出的异常。
Future<?> future = executor.submit(task);
try {
future.get();
} catch (ExecutionException e) {
// 处理任务抛出的异常
} catch (InterruptedException e) {
// 处理线程被中断的情况
}
16. 如何监控线程池的状态?有哪些常见的方法来获取线程池的运行时信息?
回答:
监控线程池的状态可以帮助了解线程池的工作情况,并根据需要进行调整。常见的监控方法包括:
-
获取线程池状态:
getPoolSize()
: 返回线程池中当前的线程数,包括空闲线程。getActiveCount()
: 返回正在执行任务的线程数。getTaskCount()
: 返回线程池已经处理和正在处理的任务总数。getCompletedTaskCount()
: 返回已经完成执行的任务数。getQueue().size()
: 返回任务队列中的等待任务数。
-
设置钩子方法:
- 可以通过重写
ThreadPoolExecutor
的beforeExecute
和afterExecute
方法,在任务执行前后记录状态信息,甚至处理异常。
- 可以通过重写
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
// 任务执行前的监控逻辑
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
// 任务执行后的监控逻辑
}
- 使用JMX监控: Java 提供了
JMX
(Java Management Extensions),可以通过它来远程监控线程池的状态。通过注册ThreadPoolExecutor
的MBean
,可以在JMX控制台中查看线程池的各种状态信息。
17. Executors
类提供了哪些方法来创建常见类型的线程池?
回答:Executors
类提供了以下常见方法来创建不同类型的线程池:
-
newFixedThreadPool(int nThreads)
: 创建一个固定大小的线程池,线程数为nThreads
。适用于执行长期稳定的任务。 -
newCachedThreadPool()
: 创建一个缓存线程池,线程池大小会根据需要自动调整,适用于大量短期任务的场景。 -
newSingleThreadExecutor()
: 创建一个单线程的线程池,适用于需要顺序执行任务的场景。 -
newScheduledThreadPool(int corePoolSize)
: 创建一个定时调度的线程池,适用于需要定期执行任务的场景。
18. 如何避免线程池中的线程过多导致的资源耗尽?
回答:
为了避免线程池中的线程过多导致系统资源耗尽,可以采取以下措施:
-
合理设置
maximumPoolSize
: 根据系统的硬件资源(如CPU、内存)和任务特性(如CPU密集型、I/O密集型)来设置线程池的最大线程数。 -
使用有界队列: 使用有界的任务队列(如
ArrayBlockingQueue
)限制待执行任务的数量,避免任务队列过多堆积。 -
监控和调整: 通过监控线程池的运行状态,动态调整线程池的大小和任务队列的容量。
-
设置合理的拒绝策略: 选择合适的拒绝策略,在系统负载过高时拒绝新任务,避免资源耗尽。
-
优化任务代码: 优化任务的执行效率,减少任务执行时间,从而提高线程池的处理能力。
19. 什么是线程饥饿死锁?如何避免?
回答:
线程饥饿死锁是指当线程池中的所有线程都在等待彼此完成某项任务时,导致线程池无法继续执行其他任务,最终所有线程陷入等待状态,系统出现死锁。
避免线程饥饿死锁的方法包括:
-
避免任务之间的相互依赖: 尽量减少任务之间的相互依赖,确保每个任务可以独立完成。
-
使用合理的线程池大小: 确保线程池中的线程数量足以处理所有可能的依赖关系,避免因线程数量不足导致的死锁。
-
避免长时间锁定: 在任务中避免长时间持有锁,或者在等待锁时设置超时时间,以减少死锁的可能性。
20. 在并发环境中,如何设计一个健壮的线程池管理机制?
回答:
设计一个健壮的线程池管理机制需要考虑以下几个方面:
-
合理配置线程池参数: 根据系统的具体需求和硬件资源合理配置
corePoolSize
、maximumPoolSize
、keepAliveTime
和任务队列大小。 -
动态调整线程池: 通过监控线程池的状态,根据系统负载动态调整线程池的参数,以应对流量的波动。
-
有效的拒绝策略: 配置合适的拒绝策略,在系统高负载时能够有效处理无法执行的任务。
-
任务优先级管理: 设计任务优先级机制,确保高优先级任务能够及时执行。
-
任务超时处理: 设置任务执行的超时时间,避免长时间执行的任务占用线程资源。
-
日志和报警机制: 在任务执行过程中记录关键日志,并在出现异常或系统负载过高时触发报警,以便及时处理问题。
通过以上措施,可以设计出一个高效、稳定、灵活的线程池管理机制,确保系统在高并发环境下的稳定运行。
让我们一起学习,一起进步!期待在评论区与你们见面。
祝学习愉快!