优先用 Runnable 而非继承 Thread,因其更灵活且符合组合优于继承原则;需显式调用 start() 启动线程,避免直接调用 run();推荐使用 ThreadPoolExecutor 替代手动创建 Thread,并注意 ThreadLocal 使用后必须 remove() 防内存泄漏。

Thread 和 Runnable 到底该用哪个
直接继承 Thread 类会占用唯一的类继承机会,而实现 Runnable 接口更灵活,也符合“组合优于继承”的设计原则。除非你需要重写 Thread 的生命周期方法(比如 start() 或 run() 的封装逻辑),否则一律优先用 Runnable。
常见错误是把耗时逻辑写在构造函数里,误以为线程已启动——其实必须显式调用 start(),而不是直接调用 run()。后者只是普通方法调用,不会新建线程。
-
new Thread(() -> { /* 逻辑 */ }).start();是最简写法,适合一次性任务 - 若需复用逻辑,定义独立类实现
Runnable,再传给Thread构造器 - 避免在
run()中吞掉异常:未捕获的RuntimeException会导致线程静默终止,建议加顶层try-catch
ExecutorService 比手动 new Thread 强在哪
手动创建 Thread 对象成本高,且无法复用、难管理。而 ExecutorService 提供线程池抽象,核心优势是资源复用、任务排队、拒绝策略和统一关闭机制。
别直接用 Executors.newFixedThreadPool() —— 它底层用的是无界 LinkedBlockingQueue,任务积压会 OOM。生产环境应显式构造 ThreadPoolExecutor,控制队列容量和拒绝行为。
立即学习“Java免费学习笔记(深入)”;
- 常用配置:
corePoolSize = CPU 核数 + 1(CPU 密集型)或2 * CPU 核数(IO 密集型) - 拒绝策略选
AbortPolicy(抛异常)或CallerRunsPolicy(由提交线程执行),避免丢任务还不报错 - 务必调用
shutdown()+awaitTermination(),否则 JVM 可能不退出
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 8,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy()
);CompletableFuture 如何真正替代 Future.get()
Future.get() 是阻塞调用,一等就卡死线程;CompletableFuture 支持非阻塞链式编排,这才是现代异步编程的关键。
容易踩的坑是混用线程上下文:默认使用 ForkJoinPool.commonPool(),但 Web 应用中常需绑定请求上下文(如 RequestContextHolder),这时必须显式指定自定义线程池,并确保 copy 上下文到新线程。
- 用
supplyAsync(() -> doWork(), executor)替代executor.submit(() -> {...}),获得可组合的返回值 -
thenApply/thenAccept在前一个任务完成后同步执行;thenApplyAsync才会提交到线程池异步执行 - 不要在回调里做耗时 IO,否则会阻塞线程池;IO 操作应再次包装成
CompletableFuture并用thenCompose
ThreadLocal 为什么不是“线程私有变量”的银弹
ThreadLocal 确实提供线程隔离,但它不解决内存泄漏问题——尤其在线程池场景下。线程复用导致 ThreadLocal 的 value 长期持有引用,若 value 是大对象或含外部引用,就会堆积。
Spring 的 RequestContextHolder、MyBatis 的 SqlSession 都依赖 ThreadLocal,但它们都配套做了清理动作。你自己用时,必须配对调用 remove(),不能只靠 set(null)。
- 在
finally块中调用threadLocal.remove(),防止异常跳过清理 - Web 场景下,在
Filter或Interceptor的afterCompletion阶段清理 - 注意子线程不继承父线程的
ThreadLocal值,如需传递,改用InheritableThreadLocal(但注意它也不自动清理)
线程池 + ThreadLocal + 忘记 remove,是线上内存泄漏最隐蔽的组合之一。











