Java常用多线程类库与Thread、Runnable

Thread是线程载体和执行控制器,Runnable仅为任务契约;前者绑定OS线程、重量级,后者无状态、可复用、支持函数式组合与线程池集成。

Thread 和 Runnable 的本质区别在哪

根本不是“哪个更好用”的问题,而是职责分离:Thread 是线程的载体和执行控制器,Runnable 只是任务契约。你不能把 Runnable 当作线程启动,必须交给 Thread 或线程池来运行。

常见错误是直接 new Runnable 并调用 run() —— 这只是普通方法调用,完全不走多线程流程;正确做法是 new Thread(runnable).start()

  • Thread 继承自 Object,本身是重量级类,每个实例都绑定一个 OS 线程资源
  • Runnable 是函数式接口,只定义 void run(),无状态、可复用、适合组合(比如包装日志、计时、异常捕获)
  • 继承 Thread 会浪费单继承机会;实现 Runnable 更灵活,也兼容 ExecutorService

为什么 Executors.newFixedThreadPool 不该在生产环境直接用

它底层用的是无界队列 LinkedBlockingQueue,任务持续涌入时,队列无限增长,极易 OOM。JDK 自己在文档里就标了 @Deprecated(从 Java 21 起正式废弃)。

替代方案不是“换一个工厂方法”,而是明确控制队列容量和拒绝策略:

  • ThreadPoolExecutor 显式构造,传入有界队列(如 new ArrayBlockingQueue(100)
  • 拒绝策略别用默认的 AbortPolicy(抛 RejectedExecutionException),根据场景选 CallerRunsPolicy 或自定义丢弃+告警逻辑
  • 线程名务必自定义:通过 ThreadFactory 设置前缀,否则堆栈里全是 pool-1-thread-1,线上排查时寸步难行

CompletableFuture 和 ExecutorService 搭配使用的坑

CompletableFuture 默认使用 ForkJoinPool.commonPool(),这个池子共享且不可配置。一旦有 CPU 密集型任务长期占用,会拖垮所有用它的异步逻辑(包括 JSON 序列化、Stream 并行操作等)。

真正可控的做法是:

  • 所有 IO 型异步操作(如 HTTP 调用、DB 查询)必须指定自定义线程池:
    CompletableFuture.supplyAsync(() -> doHttpCall(), ioExecutor)
  • 避免混用 thenApply(同步执行)和 thenApplyAsync(异步执行)——前者在上游线程中执行,可能阻塞关键路径
  • 不要在 handlewhenComplete 里做耗时操作,它们默认沿用上游线程,容易导致线程饥饿

ThreadLocal 在线程池里不清理就是内存泄漏

线程池复用线程,而 ThreadLocalEntry 是以线程为 key 的弱引用,但 value 是强引用。如果

任务没手动 remove(),value 就一直挂着,GC 不掉。

典型泄漏场景:用 ThreadLocal 格式化时间,又忘了清理。

  • 必须在 finally 块或 try-with-resources 中调用 threadLocal.remove()
  • 更安全的做法是封装工具类,在 Runnable 包装器中统一 before/after 处理
  • Spring 的 RequestContextHolder 就是靠 Filter 在请求结束时自动 reset(),自己写框架也要照这个思路

线程池 + ThreadLocal 是高频泄漏组合,比 static 引用更隐蔽,因为堆 dump 里看不到明显 GC Root,得看线程栈和 ThreadLocalMap 的实际 entry 数量。