Java创建线程有四种方式:继承Thread类、实现Runnable接口、实现Callable配合FutureTask、使用线程池;关键均需调用start()或submit()触发调度,否则不真正启动线程。

Java里创建线程不是只有 new Thread() 一种方式,但也不是所有“看起来像创建线程”的写法都真正起效——关键看是否调用了 start(),以及是否绕过了线程调度机制。
直接继承 Thread 类:最直白但有单继承限制
这是初学者最容易上手的方式,但 Java 不支持多继承,一旦类已继承其他父类(比如 MyService extends BaseService),就无法再 extends Thread。
常见错误是写了 run() 却调用 run() 而非 start():这不会开启新线程,只是在当前线程同步执行方法体。
- 必须重写
run()方法,不能改名或加参数 - 启动必须用
thread.start(),不是thread.run() - 多个
Thread实例之间不共享状态,适合隔离任务
实现 Runnable 接口:推荐的轻量级方案
比继承更灵活,适用于已有继承体系的类;也是 ThreadPoolExecutor 底层接受的任务类型。注意它本身不表示线程,只是“可被线程执行的任务”。
立即学习“Java免费学习笔记(深入)”;
容易混淆的点:把 Runnable 实例直接传给 Thread 构造器没问题,但若误传给 FutureTask 或线程池时没包装好,会编译失败或运行时报 ClassCastException。
-
new Thread(new MyTask()).start()是合法组合 - Lambda 表达式可简化为
() -> { /* logic */ },但要注意闭包变量的线程安全性 - 无法从
run()返回值,如需结果请用Callable
实现 Callable + FutureTask:需要返回值和异常处理的场景
Callable 和 Runnable 的核心区别是:支持泛型返回值、声明受检异常。但它不能直接交给 Thread,必须包装成 FutureTask(它同时实现了 Runnable 和 Future)。
典型坑:调用 future.get() 会阻塞当前线程,如果没设超时,可能卡死;另外 FutureTask 只能 run() 一次,重复调用无效。
-
new FutureTask(new MyCallable())后,仍需用new Thread(task).start() - 线程池中建议直接提交
Callable,由ExecutorService.submit()自动包装 - 异常不会在子线程打印,必须通过
get()捕获ExecutionException
使用线程池(ExecutorService):生产环境唯一合理的选择
手动 new Thread() 在高并发下极易 OOM(线程栈内存耗尽)或上下文切换开销爆炸。JDK 提供的 Executors 工厂方法只是快捷入口,真正要控制资源必须用 ThreadPoolExecutor 构造函数。
很多人用 Executors.newCachedThreadPool() 处理短任务,却忽略它默认的 60s 空闲存活时间,在突发流量后线程不回收,长期占用资源。
- 避免使用
Executors.newFixedThreadPool(10)—— 它内部用无界队列,任务积压会导致内存溢出 - 自定义线程池务必指定
BlockingQueue容量、RejectedExecutionHandler策略 -
submit(Runnable)返回Future>,submit(Callable)返回Future
真正难的不是写出四种写法,而是理解每种背后的调度模型、生命周期管理和资源边界。比如 FutureTask 的状态机、线程池的 corePoolSize 与 maximumPoolSize 如何联动、甚至 ThreadLocal 在线程复用场景下的残留风险——这些才是上线前必须验证清楚的部分。










