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