Runnable 接口实现任务与执行分离,run() 是普通方法须由 Thread.start() 或线程池调用才并发执行;可复用于多种执行器;lambda 需满足 effectively final;相比继承 Thread,Runnable 更灵活且符合解耦设计。

直接用 Runnable 接口就能把“做什么”和“谁来跑”彻底分开——任务逻辑不依赖 Thread 类,也不参与启动、中断、等待等生命周期操作,只管定义 run() 里的业务代码。
为什么不能直接调用 run() 方法
很多人写完 Runnable 实现类后,下意识调用 task.run(),结果发现:没新线程、没并发、主线程卡死、System.out.println 还是顺序执行。这是因为 run() 只是个普通方法,调它等于同步执行,跟多线程完全无关。
- 必须通过
new Thread(runnable).start()启动,JVM 才会真正创建 OS 线程并调度执行 -
start()内部触发 JVM 底层线程创建流程,之后才回调run() - 如果用线程池(如
ExecutorService),也是靠它内部的 Worker 线程去调run(),不是你手动调
Runnable 实例如何被多个执行器复用
一个 Runnable 对象可以传给 Thread、ExecutorService、ScheduledExecutorService,甚至测试时直接在主线程里调 run() 模拟执行——因为它的职责就是“可执行逻辑”,不绑定任何运行环境。
- 传给
Thread:new Thread(task, "worker-1").start() - 传给线程池:
executor.submit(task)(注意submit(Runnable)返回Future>,无返回值) - 传给定时器:
scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS) - 测试时直接调:
task.run()—— 方便单元测试,无需启动真实线程
lambda 写法下怎么捕获外部变量又不踩坑
用 lambda 实现 Runnable 很简洁,但变量捕获规则容易出错:只能访问“实际上的 final”变量,否则编译报错 local variables referenced from a lambda expression must be final or effectively final。
- 基本类型或引用类型变量,只要声明后没再赋值,就满足“effectively final”
- 别在 lambda 里改局部变量,比如
i++或list = new ArrayList() - 需要状态更新?改用原子类(
AtomicInteger)、成员变量,或封装进对象里传递 - 示例正确写法:
Runnable task = () -> System.out.println("count = " + count);(前提是count是 final 或未再赋值)
继承 Thread 类和实现 Runnable 的关键区别在哪
最根本的区别是:继承 Thread 让你的类既是任务又是线程控制器,而 Runnable 强制你只做任务;Java 单继承限制也让后者更灵活。
- 继承
Thread:类已占用extends位置,无法再继承其他业务基类 - 实现
Runnable:可同时extends BusinessService且implements Runnable -
Thread子类里重写的run()容易误写成start()调用,导致无限递归或栈溢出 - 线程池等高级并发工具只接受
Runnable或Callable,不接受Thread子类实例
真正解耦的关键不在语法,而在设计意识:当你写 Runnable 实现类时,要克制住“我想控制线程”的冲动——那个类里不该出现 interrupt()、join()、isAlive(),也不该持有 Thread 引用。这些都该由调度方决定。

















