面试题答案
一键面试线程安全机制
- 原子变量:ThreadPoolExecutor 使用
AtomicInteger
类型的变量ctl
来存储线程池的状态(如运行状态、线程数等)。通过AtomicInteger
的原子操作,如compareAndSet
方法,确保对线程池状态和线程数量的修改是线程安全的。例如,在添加新线程时,会使用ctl
的compareAndSet
操作来尝试增加线程数量,只有当当前状态和预期状态一致时才会修改成功,避免了并发修改的冲突。 - 锁机制:在一些关键操作上,如任务的提交、线程的创建和销毁等,使用了
ReentrantLock
进行同步控制。例如,在execute
方法中,通过获取锁来保证在同一时间只有一个线程能够对线程池的状态进行修改,比如添加新任务到队列或者创建新线程。锁的使用确保了对线程池内部数据结构(如任务队列)的安全访问。 - volatile 变量:ThreadPoolExecutor 中的一些关键变量,如
corePoolSize
、maximumPoolSize
等被声明为volatile
。这保证了线程对这些变量的修改能够及时被其他线程可见,避免了线程从自己的工作内存中读取到过期数据。例如,当动态调整corePoolSize
时,其他线程能够立即感知到这个变化。
资源高效利用及避免问题
- 避免线程饥饿:
- 任务队列:合理选择任务队列(如
LinkedBlockingQueue
、SynchronousQueue
等)。对于LinkedBlockingQueue
,它是无界队列,当任务较多时,新任务会被放入队列而不是直接创建新线程直到maximumPoolSize
,这样可以避免过度创建线程导致部分线程长期得不到任务而饥饿。SynchronousQueue
则不存储任务,任务提交后必须立即有线程来处理,这促使线程池尽快创建新线程处理任务,减少任务等待时间,也避免了线程饥饿。 - 拒绝策略:当任务队列已满且线程数达到
maximumPoolSize
时,合理的拒绝策略可以避免线程饥饿。例如,AbortPolicy
直接抛出异常,提醒调用者任务处理失败,调用者可以根据情况调整策略;CallerRunsPolicy
让提交任务的线程自己执行任务,这样可以减少新任务堆积,避免线程饥饿。
- 任务队列:合理选择任务队列(如
- 避免锁争用:
- 分离锁:ThreadPoolExecutor 采用了分离锁的思想。例如,任务的提交和线程的创建销毁等操作使用不同的锁机制。任务提交主要涉及任务队列的操作,通过
ReentrantLock
进行同步;而线程的创建和销毁则依赖于ctl
的原子操作和一些其他的局部同步机制,这样可以减少锁争用的范围。 - 减少锁持有时间:在关键操作中,尽量缩短锁的持有时间。例如,在
execute
方法中,获取锁后尽快完成任务添加到队列或创建新线程的操作,然后释放锁,避免长时间持有锁导致其他线程等待。
- 分离锁:ThreadPoolExecutor 采用了分离锁的思想。例如,任务的提交和线程的创建销毁等操作使用不同的锁机制。任务提交主要涉及任务队列的操作,通过
优化思路结合应用场景
- 高并发短任务场景:例如 Web 服务器处理大量的 HTTP 请求,这些请求处理时间短但并发量高。可以将
corePoolSize
设置得相对较大,使用SynchronousQueue
作为任务队列。这样可以让新任务快速分配到线程处理,减少任务在队列中的等待时间。同时,适当调整maximumPoolSize
,以应对突发的高并发情况。拒绝策略可以选择CallerRunsPolicy
,让提交任务的线程在必要时自己处理任务,避免请求丢失。 - 低并发长任务场景:比如一些数据处理的后台任务,任务处理时间长但并发量低。可以将
corePoolSize
设置得较小,使用LinkedBlockingQueue
作为任务队列,让任务在队列中等待,避免过多线程创建导致系统资源浪费。此时拒绝策略可以选择AbortPolicy
,因为任务处理时间长,系统资源有限,不适合让提交任务的线程自己处理任务,抛出异常可以让开发者及时调整任务调度策略。