Java线程与线程池:从入门到"避坑"全攻略
一、线程与线程池:基础篇
1. 线程是什么?
线程是程序执行的最小单位,好比快递公司的快递员。一个进程(比如一个Java程序)可以有多个线程,共享进程的堆和方法区,但每个线程有自己的程序计数器、虚拟机栈等"私人行李"。
为什么需要线程?
- 效率高:线程切换成本低,多核CPU时代可并行处理任务。
- 资源利用率:比如单核CPU下,一个线程等IO时,其他线程能抢CPU干活。
2. 线程池:快递公司的管理智慧
频繁创建销毁线程就像每天招快递员再开除,太浪费!线程池就是"快递公司",核心线程是正式工,临时线程是兼职,队列是排期表,拒绝策略是"拒单"。
线程池的优势:
- 资源复用:线程用完不销毁,下次直接调用。
- 可控性:限制线程数量,避免系统被"快递员挤爆"。
二、线程池用法:从"Hello World"到实战
1. 手动创建线程池(拒绝Executors!)
阿里规范警告:Executors
快捷方法会埋雷!比如FixedThreadPool
用无界队列,任务堆积可能引发OOM。
正确姿势:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // 核心线程数(正式工)
20, // 最大线程数(正式工+临时工)
60, TimeUnit.SECONDS, // 临时工摸鱼时间
new LinkedBlockingQueue<>(100), // 有界队列(排期表容量)
new ThreadFactoryBuilder().setNameFormat("订单处理-%d").build(), // 给线程起名,方便查户口
new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时,让老板亲自送货
);
2. 提交任务的三种姿势
- Runnable:只管干活,不问结果(适合发短信通知)。
- Callable:干完活还能带个返回值(适合查库存)。
- CompletableFuture:异步编程神器,支持链式调用(适合组合多个任务)。
三、原理揭秘:线程池的"五脏六腑"
1. 线程池工作流程
- 任务来了,先找核心线程(正式工)。
- 正式工满了?任务进队列(排期表)。
- 队列满了?招临时工(直到最大线程数)。
- 连临时工都招满了?拒绝策略出场(比如"老板亲自送货")。
2. Worker线程:打工人的一生
每个Worker是一个"无限循环"的线程,核心逻辑是:
java
while (task != null || (task = getTask()) != null) {
try {
task.run(); // 干活!
} finally {
task = null; // 干完活等下一个任务
}
}
- 摸鱼检测 :临时工如果
keepAliveTime
内没活干,会被开除。
四、避坑指南:别让线程池变"雷池"
1. OOM三大惨案
- 无界队列 :
LinkedBlockingQueue
默认容量是Integer.MAX_VALUE
,任务堆积直接内存爆炸。 - 线程数失控 :
CachedThreadPool
允许创建Integer.MAX_VALUE
个线程,系统直接卡死。
2. 死锁的"父子相残"
案例:父任务占满线程池,子任务在队列等待,互相等对方释放资源------解决方法是用两个线程池,父子任务分开处理。
3. 参数配置玄学
- CPU密集型:核心线程数 = CPU核数 + 1(避免上下文切换)。
- IO密集型 :核心线程数 = CPU核数 * 2(比如处理网络请求)。
公式:最佳线程数 = CPU核数 * (1 + 平均等待时间 / 平均计算时间)。
五、最佳实践:美团大佬的"骚操作"
1. 线程池命名
默认线程名像"路人甲",用ThreadFactoryBuilder
起名,比如"订单处理-1",日志排查时一目了然。
2. 监控线程池状态
定时打印线程数、队列大小等指标,早发现早治疗:
java
scheduledExecutorService.scheduleAtFixedRate(() -> {
log.info("活跃线程: {}", executor.getActiveCount());
log.info("队列任务: {}", executor.getQueue().size());
}, 0, 1, TimeUnit.SECONDS);
六、面试考点:征服面试官的"灵魂拷问"
1. 线程状态图
- NEW :刚出生,还没
start()
。 - RUNNABLE:可运行(可能在等CPU)。
- BLOCKED :等锁(比如
synchronized
)。 - WAITING :等唤醒(
wait()
或join()
)。 - TIMED_WAITING :睡一会(
sleep(1000)
)。 - TERMINATED:领盒饭了。
2. sleep vs wait
- sleep:抱着锁睡觉(不释放锁),到点自动醒。
- wait :释放锁,等别人
notify()
。
3. 死锁四条件
- 互斥、占有且等待、不可剥夺、循环等待。
破解方法:按顺序申请锁(比如统一先申请锁A,再申请锁B)。
七、总结:线程池的"生存法则"
线程池是并发编程的瑞士军刀,但用不好会变成"自爆按钮"。记住三条黄金法则:
- 手动创建 :拒绝
Executors
,拥抱ThreadPoolExecutor
。 - 监控命名:线程池要有"身份证",运行状态要透明。
- 业务隔离:不同业务用不同池,避免"一损俱损"。
最后,线程池不是银弹,合理配置才能让系统"稳如老狗"。愿你的代码永无OOM,线程永不死锁!
参考资料: