Java线程与线程池:从入门到“避坑”全攻略

21 阅读4分钟

Java线程与线程池:从入门到“避坑”全攻略


一、线程与线程池:基础篇

1. 线程是什么?

线程是程序执行的最小单位,好比快递公司的快递员。一个进程(比如一个Java程序)可以有多个线程,共享进程的堆和方法区,但每个线程有自己的程序计数器、虚拟机栈等“私人行李”。
为什么需要线程?

  • 效率高:线程切换成本低,多核CPU时代可并行处理任务。
  • 资源利用率:比如单核CPU下,一个线程等IO时,其他线程能抢CPU干活。

2. 线程池:快递公司的管理智慧

频繁创建销毁线程就像每天招快递员再开除,太浪费!线程池就是“快递公司”,核心线程是正式工,临时线程是兼职,队列是排期表,拒绝策略是“拒单”。
线程池的优势

  • 资源复用:线程用完不销毁,下次直接调用。
  • 可控性:限制线程数量,避免系统被“快递员挤爆”。

二、线程池用法:从“Hello World”到实战

1. 手动创建线程池(拒绝Executors!)

阿里规范警告:Executors快捷方法会埋雷!比如FixedThreadPool用无界队列,任务堆积可能引发OOM。
正确姿势

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. 线程池工作流程

  1. 任务来了,先找核心线程(正式工)。
  2. 正式工满了?任务进队列(排期表)。
  3. 队列满了?招临时工(直到最大线程数)。
  4. 连临时工都招满了?拒绝策略出场(比如“老板亲自送货”)。

2. Worker线程:打工人的一生

每个Worker是一个“无限循环”的线程,核心逻辑是:

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. 监控线程池状态

定时打印线程数、队列大小等指标,早发现早治疗:

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)。

七、总结:线程池的“生存法则”

线程池是并发编程的瑞士军刀,但用不好会变成“自爆按钮”。记住三条黄金法则:

  1. 手动创建:拒绝Executors,拥抱ThreadPoolExecutor
  2. 监控命名:线程池要有“身份证”,运行状态要透明。
  3. 业务隔离:不同业务用不同池,避免“一损俱损”。

最后,线程池不是银弹,合理配置才能让系统“稳如老狗”。愿你的代码永无OOM,线程永不死锁!


参考资料