多线程八股
多线程八股
1.ArrayList的底层原理
-
Array List底层是用动态扩展的数组实现的;
-
ArrayList初始容量为0,当第一次添加数据的时候才初始容量为10;
-
在进行扩展时容量是原来的1.5倍,每次扩展都需要拷贝数据;
-
在添加数据的时候有以下情况:首先判断当前数组是否有足够容量存储新数据,如果容量不足,将调用grow方法进行扩容(原来的1.5倍),确保新增数据有地方存储后,将新元素添加到位于size的位置上,返回添加成功布尔值。
为什么ArrayList的插入有时比LinkedList快?
A:当插入位置靠近尾部且不需要扩容时,ArrayList的System.arraycopy()
在现代CPU上的效率可能高于LinkedList的内存分配和指针操作,尤其在小数据集时。
何时LinkedList实际插入复杂度不是O(1)?
A:当需要先定位插入位置时(如list.add(index, element)
),需要O(n)遍历时间,实际操作为O(n)+O(1)
2.多线程
核心思想:多线程就是让程序能“同时”干好几件事(虽然CPU可能在快速切换)。关键问题在于怎么让它们别打架(线程安全),怎么排队(锁),怎么互相打招呼(协作),怎么高效干活(线程池)。
1. 线程安全:为啥要“锁”?
- 问题本质: 想象一个公共厕所(共享资源),很多人(线程)想用。如果大家一窝蜂冲进去,或者不锁门,会发生啥?混乱!程序里就是数据被改得乱七八糟(脏数据)。
- 经典例子:
i++
这个操作。它看起来简单,实际是三步:读i的值 -> 加1 -> 写回i。如果两个线程同时做这个操作,可能它们都读到同一个值(比如10),都加1变成11,然后都写回去。结果应该是12,但实际只有11!这就是竞态条件。 - 面试官爱问:
HashMap
为啥线程不安全?ConcurrentHashMap
怎么解决的?- 大白话:
HashMap
就像个没管理员也没排队规则的菜市场摊位,一群人(线程)同时抢着往本子上记东西(修改内部结构),很容易把本子撕坏(内部链表环化导致死循环)或者记错(数据丢失/错误)。ConcurrentHashMap
聪明多了:它给菜市场分了很多小隔间(分段锁/JDK8后的Node+CAS),大家只在抢同一个隔间的东西时才需要排队,抢不同隔间的可以同时进行,效率高多了。或者用更精细的“无锁”方式(CAS)来记账。
- 大白话:
- 核心考点: 理解多个线程同时修改共享数据会出乱子。怎么解决?用“锁”或者“原子操作”。
2. 锁:怎么“排队”和“管理钥匙”?
synchronized
(内置锁/监视器锁):- 比喻: 就像厕所门上的一把钥匙。谁想进去,必须拿到这把钥匙,用完出来再把钥匙挂回去(释放锁)。别人(其他线程)想进去?只能等着(阻塞)。
- 怎么用?
- 锁方法: 在方法前加
synchronized
-> 钥匙就是这个方法所属的对象本身(对于实例方法) 或 这个类的Class对象(对于静态方法)。 - 锁代码块:
synchronized(某个对象) { ... }
-> 钥匙就是你指定的那个对象(锁对象)。
- 锁方法: 在方法前加
- 特性:
- 可重入: 同一个线程拿到钥匙后,可以再进这个锁保护的另一个门(调用另一个
synchronized
方法或代码块),不会被自己卡在外面。就像你有钥匙,进大门后进里面的小门不需要再找钥匙。 - 非公平: 默认情况下,外面等钥匙的线程不是严格按先来后到的顺序拿钥匙。谁抢到算谁的(虽然JVM有优化,但本质非公平)。
- 可重入: 同一个线程拿到钥匙后,可以再进这个锁保护的另一个门(调用另一个
ReentrantLock
(可重入锁):- 比喻:
synchronized
的升级版。还是那把钥匙,但功能更强:- 公平锁: 可以设置成严格排队,先来的线程先拿钥匙。避免“饥饿”(老实的线程永远抢不到)。
- 可中断: 等钥匙等烦了(阻塞状态),可以喊一声“不等了!” (
lockInterruptibly()
) 然后去做别的事。 - 超时等待: 可以设定等钥匙的最长时间,时间到了还没拿到就走人 (
tryLock(timeout)
)。 - 条件变量 (
Condition
): 更精细的“等待室”。比如生产者线程发现仓库满了,就去“满仓等待室”等着;消费者拿走东西后,可以去那个“满仓等待室”喊一声“有位置了!”,只唤醒在等仓库空位的生产者,而不是乱喊把所有人都吵醒(wait/notify
是唤醒所有在同一个锁上等待的线程)。
- 注意: 用
ReentrantLock
必须手动解锁!通常放在finally
块里。synchronized
是自动释放的。
- 比喻:
- 核心考点: 理解
synchronized
和ReentrantLock
的基本用法、区别(公平性、灵活性、需手动释放)、可重入性。知道ReentrantLock
的高级功能(公平、中断、超时、条件)。
3. volatile
:这个变量大家都能看见最新版!
- 问题本质: CPU有高速缓存。线程A修改了一个变量,可能先存在自己的缓存里,还没写回主内存。线程B去读主内存,读到的还是旧值!这就不一致了。
volatile
的作用:- 可见性: 保证一个线程修改了
volatile
变量,新值立刻对其他所有线程可见(强制写回主存,其他线程读时强制从主存读)。 - 禁止指令重排序: 编译器/JVM为了优化,可能会调整代码执行顺序。
volatile
能防止这种重排序跨越它(建立内存屏障)。
- 可见性: 保证一个线程修改了
- 大白话: 给变量贴了个“公告栏”。谁改了它,必须把新值写在公告栏上(主存)。谁要看它,必须去看公告栏,不能看自己小本本(缓存)上的旧记录。同时告诉编译器和CPU:“这个变量周围的代码顺序别乱动!”
- 注意:
volatile
不保证原子性! 它只保证单次读/写是原子的(比如读一个long)。像i++
(读+改+写)这种复合操作,volatile
管不了。要保证i++
原子性,得用synchronized
或AtomicInteger
。 - 典型场景: 状态标志位(如
boolean running = true;
需要volatile
),单例模式双重检查锁定(DCL)里的实例引用。 - 核心考点: 理解
volatile
解决了什么问题(可见性、有序性),不解决什么问题(原子性)。常见使用场景。
4. 线程间通信:你干完了叫我一声!
wait()
,notify()
,notifyAll()
(必须在synchronized
块里用):- 比喻: 还是那个公共厕所(锁对象)。线程A拿到钥匙进去后发现没纸了(
条件不满足
)。wait()
:A把钥匙挂回去(释放锁),然后去厕所旁边的“等待室”坐着睡觉。notify()
:线程B(比如送纸工)拿到钥匙进去送完纸,出来时可以喊一声“纸来啦!”(notify()
),这会随机唤醒等待室里的一个线程(比如A)。notifyAll()
:喊“纸来啦!”,唤醒等待室里所有线程。被唤醒的线程会重新竞争钥匙。
- 要点:
- 调用
wait()
前必须持有锁(在synchronized
块里)。 wait()
会释放锁。- 被唤醒的线程在从
wait()
返回前必须重新拿到锁。 - 通常用
while(条件不满足) { wait(); }
来防止虚假唤醒(没被notify
也可能醒来)。
- 调用
- 比喻: 还是那个公共厕所(锁对象)。线程A拿到钥匙进去后发现没纸了(
Condition
(配合ReentrantLock
使用):- 比喻:
ReentrantLock
可以有多个独立的“等待室”(Condition)。- 生产者:
notFull.await()
(仓库满了,去“不满等待室”等着) -> 消费者取货后:notFull.signal()
(喊“不满啦!”唤醒生产者)。 - 消费者:
notEmpty.await()
(仓库空了,去“不空等待室”等着) -> 生产者放货后:notEmpty.signal()
(喊“不空啦!”唤醒消费者)。
- 生产者:
- 优点: 比
wait/notify
更精细,能定向唤醒特定条件的线程。
- 比喻:
- 核心考点: 理解
wait/notify
机制(为什么要在synchronized
里?为什么wait
会释放锁?虚假唤醒?)。知道Condition
提供了更灵活的等待/通知方式。
5. 原子类 (java.util.concurrent.atomic
):不用锁也能安全加减?
- 问题:
i++
不安全,用synchronized
太重(排队慢)。 - 解决:
AtomicInteger
,AtomicLong
等。 - 原理: 利用CPU底层的 CAS (Compare And Swap) 指令。
- CAS比喻: 想象一个值V在内存里。线程A想把它从10改成11:
- 读当前值 (10 - 期望值)。
- 计算新值 (11)。
- 进行CAS操作:检查内存里的值现在还是不是10?如果是,说明没人改过,放心改成11!如果不是,说明被别人改过了(比如变成了12),那这次修改失败,重新再试(读新值12 -> 计算13 -> 再次尝试CAS…)。
- 这个过程是硬件级别保证原子性的。
- CAS比喻: 想象一个值V在内存里。线程A想把它从10改成11:
- 优点: 无锁(或乐观锁),性能通常比
synchronized
高很多,尤其在低竞争场景。 - 方法:
get()
,set()
,getAndIncrement()
(i++),incrementAndGet()
(++i),compareAndSet(expect, update)
(核心CAS) 等。 - 核心考点: 知道原子类存在(解决计数器等简单共享变量的原子操作),理解其底层原理是CAS,知道CAS是什么(比较并交换)。了解ABA问题(虽然不常深究)。
6. 线程池:别老创建销毁线程,太浪费!
- 为什么需要? 创建和销毁线程开销大。线程池预先创建好一些线程放着(核心线程),来任务了就让这些线程去执行。任务太多时,新任务排队(工作队列)。排队也排满了?就创建新线程(直到最大线程数)。最大线程数也满了?就拒绝新任务(拒绝策略)。
- 核心参数 (ThreadPoolExecutor):
corePoolSize
(核心线程数): 池子里长期保留的工人数,即使他们闲着。maximumPoolSize
(最大线程数): 池子最多能容纳的工人数(核心 + 临时工)。workQueue
(工作队列): 任务太多时,排队的队伍。常用LinkedBlockingQueue
(无界/有界),ArrayBlockingQueue
(有界),SynchronousQueue
(直接交接,不排队)。keepAliveTime
(空闲线程存活时间): 临时工(超出核心线程数的那些)如果闲着超过这个时间,就被解雇(销毁)。threadFactory
(线程工厂): 用来创建新线程(可以设置线程名、优先级等)。RejectedExecutionHandler
(拒绝策略): 当池子满了(线程数达max且队列也满了),新任务咋办?常见策略:AbortPolicy
(默认):直接抛异常RejectedExecutionException
。CallerRunsPolicy
:让提交任务的线程(比如main线程)自己来执行这个任务。DiscardPolicy
:默默丢掉新任务,不通知。DiscardOldestPolicy
:丢掉队列里最老的任务,然后尝试把新任务加进去。
- 常见线程池 (Executors 工厂创建,但一般推荐手动配置 ThreadPoolExecutor):
FixedThreadPool
: 固定大小线程池。核心=最大线程数,队列无界。可能导致OOM。CachedThreadPool
: 核心线程数=0,最大线程数巨大(几乎无限制),队列是SynchronousQueue
。来一个任务,如果有空闲线程就用,没有就创建新线程。线程空闲60秒后被回收。适合大量短生命周期的异步任务。可能导致创建过多线程。SingleThreadExecutor
: 只有一个线程的池子。保证任务按提交顺序串行执行。如果这个线程挂了,会创建一个新的。ScheduledThreadPool
: 能执行定时或周期性任务的线程池。
- 核心考点: 为什么用线程池?线程池的核心参数有哪些?各自代表什么?常见的线程池类型(Fixed, Cached, Single)的特点和潜在风险?如何配置一个合理的线程池?常见的拒绝策略?
7. 其他高频点:
Thread
vsRunnable
vsCallable
:Thread
:代表一个线程对象。可以直接继承Thread
并重写run()
,但Java是单继承,不推荐。Runnable
:最常用。定义一个任务(run()
方法),没有返回值,不能抛受检异常。任务可以被提交给Thread
或线程池执行。Callable
:类似Runnable
,但它的call()
方法有返回值,并且可以抛出受检异常。通常配合Future
/FutureTask
使用,由线程池 (ExecutorService.submit()
) 执行。
Future
/FutureTask
: 代表一个异步计算的结果。你可以用它来:- 查询计算是否完成 (
isDone()
)。 - 尝试取消计算 (
cancel()
)。 - 获取计算结果 (
get()
)。注意get()
会阻塞,直到计算完成或超时。
- 查询计算是否完成 (
synchronized
和ReentrantLock
的区别: 上面锁的部分已经讲了(公平性、灵活性、手动释放、条件变量)。- 死锁:
- 条件: 互斥、持有并等待、不可剥夺、循环等待。
- 避免:
- 按固定顺序获取锁(所有线程都按A->B->C的顺序申请锁)。
- 使用带超时的锁(
tryLock(timeout)
)。 - 避免嵌套锁。
ThreadLocal
: 给每个线程提供了一个变量的独立副本。线程内部任何地方都可以访问到这个副本。常用于保存线程上下文信息(如用户会话、数据库连接),避免参数传递。- 注意: 使用不当(比如在线程池环境)可能导致内存泄漏!用完记得
remove()
!
- 注意: 使用不当(比如在线程池环境)可能导致内存泄漏!用完记得
8. ThreadLocal 能给子线程传值吗?
-
核心答案:默认情况下,不能!
-
通俗解释:
- 想象
ThreadLocal
是每个线程自己专属的“储物柜”(ThreadLocalMap
)。父线程往自己的储物柜里放的东西(设置值),子线程是完全看不到、拿不到的。子线程有自己的、全新的、空空的储物柜。 - 为什么?
ThreadLocal
的设计核心就是线程隔离,保证每个线程操作自己的变量副本,互不干扰。这是它的核心价值所在(避免同步,提高性能)。
- 想象
-
特殊情况:
InheritableThreadLocal
- 可以! 这是
ThreadLocal
的一个特殊子类。 - 原理: 当父线程创建一个子线程时,
InheritableThreadLocal
会把它父线程中当前的值拷贝一份给新创建的子线程的InheritableThreadLocal
。之后,父子线程各自修改自己的副本,互不影响。 - 关键限制:
- 仅适用于“创建时”传递: 只在
new Thread()
创建子线程的那一刻进行值拷贝。之后父线程再修改自己的值,子线程不会跟着变。 - 线程池大坑! 线程池的核心在于复用线程。当一个线程被线程池创建出来时,它从父任务(可能是主线程,也可能是其他线程)那里继承了当时的
InheritableThreadLocal
值。但是,当这个线程执行完一个任务,回到线程池待命,再被分配执行下一个任务时:- 它之前执行任务时修改的
InheritableThreadLocal
值还在! - 下一个任务提交者(可能是完全不同的调用者)设置的
InheritableThreadLocal
值不会自动覆盖线程池线程已有的值。 - 这会导致严重的数据错乱和内存泄漏(旧任务的值一直残留在复用线程中)。
- 它之前执行任务时修改的
- 仅适用于“创建时”传递: 只在
- 可以! 这是
-
线程池中如何“传值”?
- 避免使用
InheritableThreadLocal
: 因为上述问题,在线程池场景下基本不可用。 - 推荐方法:
- 显式传递参数: 把需要传递的值作为
Runnable
或Callable
任务的构造参数传入。这是最清晰、最安全的方式。 - 使用第三方库 (如阿里 TransmittableThreadLocal - TTL): 专门为解决线程池上下文传递设计。它通过包装
Runnable
/Callable
在任务提交时捕获当前值,在任务执行时恢复值,并在执行后清理,完美适配线程池复用机制。(面试加分项!)
- 显式传递参数: 把需要传递的值作为
- 避免使用
-
总结: 普通
ThreadLocal
绝对不行。InheritableThreadLocal
只在new Thread()
创建子线程时有效,严禁用于线程池。线程池传值首选显式参数传递,复杂场景考虑 TTL 等方案。
- 线程池怎么创建?**
-
核心答案:强烈推荐使用
ThreadPoolExecutor
构造器手动创建! -
为什么不用
Executors
工厂方法?Executors.newFixedThreadPool()
和Executors.newSingleThreadExecutor()
: 使用无界队列 (LinkedBlockingQueue
)。如果任务提交速度持续远大于处理速度,队列会无限增长,最终导致OutOfMemoryError
(OOM)。Executors.newCachedThreadPool()
: 核心线程数为0,最大线程数是Integer.MAX_VALUE
。如果任务提交过多且都是长任务,可能瞬间创建海量线程,耗尽系统资源 (CPU, 内存,线程数限制),同样可能导致崩溃。
-
正确创建姿势:
import java.util.concurrent.*;public class CustomThreadPool {public static void main(String[] args) {// 1. 定义核心参数 (下面会详细讲每个参数)int corePoolSize = 5; // 核心线程数int maximumPoolSize = 10; // 最大线程数long keepAliveTime = 60L; // 空闲线程存活时间 (单位)TimeUnit unit = TimeUnit.SECONDS; // 存活时间单位 (秒)BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 工作队列 (有界队列,容量100)ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂 (可自定义)RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略 (默认抛异常)// 2. 使用构造器创建线程池ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler);// 3. 提交任务threadPool.execute(() -> {System.out.println("执行任务...");});// ... 提交更多任务// 4. 优雅关闭 (重要!)threadPool.shutdown(); // 停止接收新任务,等待已提交任务执行完成// threadPool.shutdownNow(); // 尝试立即停止所有正在执行的任务,返回未执行任务列表} }
10. 线程池有哪些参数呢?
ThreadPoolExecutor
构造器的 7 个核心参数 (非常重要!):
corePoolSize
(核心线程数):- 池中长期保持存活的线程数量,即使它们是空闲状态。
- 相当于公司里的“正式编制员工”。
maximumPoolSize
(最大线程数):- 池中允许存在的最大线程数量。
- 当任务非常多,队列也满了,线程池会创建新线程来处理任务,直到达到这个数量。
- 相当于“正式员工” + “临时工”的总人数上限。
keepAliveTime
(空闲线程存活时间):- 当线程池中的线程数量超过
corePoolSize
时,那些多余的空闲线程在等待新任务时的最长存活时间。 - 如果超过这个时间还没有新任务,这些多余的线程就会被终止销毁。
- 只对 超过
corePoolSize
的那些线程生效。核心线程默认一直存活 (allowCoreThreadTimeOut
可设置核心线程超时)。 - 单位: 需要配合下一个参数
unit
。 - 相当于“临时工”如果闲着没事干超过一定时间,就被解雇。
- 当线程池中的线程数量超过
unit
(存活时间单位):- 指定
keepAliveTime
参数的时间单位。 - 常用
TimeUnit.SECONDS
(秒)、TimeUnit.MILLISECONDS
(毫秒) 等。
- 指定
workQueue
(工作队列):- 用于存放被提交但尚未被执行的任务的阻塞队列 (BlockingQueue)。
- 关键选择:
ArrayBlockingQueue
: 基于数组的有界队列。需要指定容量。任务数超过容量且线程数达max,触发拒绝策略。推荐!防止OOM。LinkedBlockingQueue
: 基于链表的队列。默认构造是无界队列 (Integer.MAX_VALUE
),容易OOM。也可指定容量变成有界。SynchronousQueue
: 一个不存储元素的队列。每个put
操作必须等待一个take
操作,反之亦然。相当于直接交接。通常要求maximumPoolSize
足够大,否则容易触发拒绝策略。Executors.newCachedThreadPool()
使用它。PriorityBlockingQueue
: 具有优先级的无界队列。按优先级出队。也有OOM风险。
- 相当于“任务待办事项清单”或“排队等候区”。
threadFactory
(线程工厂):- 用于创建新线程的工厂。可以实现
ThreadFactory
接口来自定义线程的名称、是否是守护线程、优先级等。 - 如果不指定,使用
Executors.defaultThreadFactory()
,创建的就是普通的、同组的、非守护线程。 - 相当于“人力资源部”,负责按照要求“招聘”(创建)新员工(线程)。
- 用于创建新线程的工厂。可以实现
handler
(拒绝策略 - RejectedExecutionHandler):- 当线程池已经关闭,或者线程池饱和(达到
maximumPoolSize
且workQueue
已满)时,对新提交的任务采取的处理策略。 - 内置策略:
AbortPolicy
(默认): 直接抛出RejectedExecutionException
异常。最常用,明确知道任务被拒绝了。CallerRunsPolicy
: 将任务退回给调用者线程(提交任务的线程,比如 main 线程)去执行。这样提交任务的速度会下降,给线程池喘息时间。DiscardPolicy
: 默默丢弃新提交的任务,不做任何通知。DiscardOldestPolicy
: 丢弃工作队列中排队时间最久(队列头部)的那个任务,然后尝试重新提交当前这个新任务。(不保证成功,因为队列可能还是满的)。
- 相当于当“待办事项清单”满了,且“员工+临时工”都用满了,再有新任务来时的“处理办法”。
- 当线程池已经关闭,或者线程池饱和(达到
11. 线程数怎么设置呢?
- 核心答案:没有绝对标准答案!需要根据具体业务场景、服务器资源进行压测和调整。但有一些指导原则:
- 考虑因素:
- 任务类型 (最关键!):
- CPU 密集型任务 (计算为主,如复杂算法、视频编码): 线程数 ≈ CPU 核心数 (或 CPU 核心数 + 1)。设置过多会导致大量线程上下文切换,反而降低性能。
Runtime.getRuntime().availableProcessors()
获取逻辑核心数。 - I/O 密集型任务 (等待为主,如网络请求、数据库操作、文件读写): 线程数可以设置得远大于 CPU 核心数。因为线程在等待 I/O 时 CPU 是空闲的,可以处理其他线程的任务。经验公式:
- 线程数 ≈ CPU 核心数 * (1 + 平均等待时间 / 平均计算时间)
- 例如:4核CPU,任务50%时间计算,50%时间等待I/O:线程数 ≈ 4 * (1 + 0.5 / 0.5) = 4 * 2 = 8
- 实际中等待时间很难精确计算,通常需要压测。可以从
CPU核心数 * 2
开始测试,逐步增加,观察 CPU 利用率、响应时间、吞吐量变化,找到性能拐点。
- CPU 密集型任务 (计算为主,如复杂算法、视频编码): 线程数 ≈ CPU 核心数 (或 CPU 核心数 + 1)。设置过多会导致大量线程上下文切换,反而降低性能。
- 系统资源限制:
- CPU 核心数: 是硬限制。过多的 CPU 密集型线程只会导致争抢和切换。
- 内存: 每个线程都需要栈内存(默认约1MB,可通过
-Xss
调整)。大量线程会消耗可观的内存。 - 操作系统/文件句柄/数据库连接池限制: 线程数不能超过这些外部资源的限制。
- 任务特性:
- 任务优先级: 是否需要优先级队列?
PriorityBlockingQueue
。 - 任务依赖: 复杂的依赖关系可能需要更复杂的线程池组合或任务编排 (如 CompletableFuture)。
- 任务执行时间: 长短任务混合?可能需要隔离不同的线程池处理。
- 任务优先级: 是否需要优先级队列?
- 业务目标:
- 吞吐量优先: 在资源允许范围内,适当增加线程数可能提升吞吐量。
- 响应时间优先: 需要控制线程数避免过多排队。可能需要设置合理的队列大小和拒绝策略保证核心请求响应。
- 突发流量:
- 考虑系统是否能承受短时间的高峰流量。可以通过
maximumPoolSize
和合适的队列大小 (ArrayBlockingQueue
) 以及拒绝策略 (CallerRunsPolicy
或自定义降级) 来应对。
- 考虑系统是否能承受短时间的高峰流量。可以通过
- 任务类型 (最关键!):
- 动态调整:
- 理想情况下,线程池大小应该能根据负载动态调整。虽然
ThreadPoolExecutor
提供了setCorePoolSize()
和setMaximumPoolSize()
方法,但动态调整逻辑需要自己实现(例如基于监控指标)。 - 很多成熟的框架(如 Spring Cloud Netflix Hystrix, 阿里 Sentinel)或服务网格(如 Istio)提供了更高级的流量控制、熔断和线程池隔离策略。
- 理想情况下,线程池大小应该能根据负载动态调整。虽然
- 实践建议:
- 优先使用有界队列 (
ArrayBlockingQueue
) 并设置合理的容量。 这是防止 OOM 的关键。 - 设置明确的拒绝策略。
AbortPolicy
或CallerRunsPolicy
通常是好的选择,让调用者感知到系统压力。 - I/O 密集型任务: 从
CPU核心数 * 2
开始压测调整。压测!压测!压测! 是找到最佳线程数的唯一可靠方法。 - CPU 密集型任务: 设置为
CPU核心数
或CPU核心数 + 1
。 - 监控: 使用 JMX、Micrometer 等工具监控线程池的关键指标(活动线程数、队列大小、任务完成数、拒绝任务数等),以便及时调整配置。
- 优先使用有界队列 (
总结关键点:
- ThreadLocal: 默认不能传值给子线程。
InheritableThreadLocal
只在新线程创建时有效,绝对不要用于线程池。线程池传值用参数传递或 TTL。 - 线程池创建: 用
ThreadPoolExecutor
手动创建,避免Executors
的潜在 OOM 风险。 - 线程池参数: 牢记 7 大金刚 (
corePoolSize
,maxPoolSize
,keepAliveTime
,unit
,workQueue
,threadFactory
,handler
) 的含义和作用,特别是有界队列和合适的拒绝策略。 - 线程数设置: 看任务类型 (CPU/IO)、看系统资源、看业务目标。CPU 密集型 ≈ 核心数;IO 密集型 ≈ 核心数 * N (N>1,需压测确定)。压测是王道!
一句话总结核心: **多线程要安全,共享数据要保护(锁/原子类/volatile)。线程之间要协作(wait/notify/Condition)。线程创建销毁太贵,用池子管理(线程池)。