图 1:本文封面,两种调用方式的执行路径对照。
先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试资料整理,与任何公司的当前招聘无关。这道题以选择题形式出现:给一段 main 方法里 t.run() 再打印字符串的代码,问输出是什么。它考的是 Java 并发里最基础也最容易想当然的一个语义陷阱。
原题还原:输出为什么是 DPMT。
原题大意如下(匿名内部类继承 Thread,重写 run 打印 DP):
public static void main(String args[]) {
Thread t = new Thread() {
public void run() {
print();
}
};
t.run();
System.out.print("MT");
}
static void print() {
System.out.print("DP");
}
选项里有 DPMT 和 MTDP 两种顺序。正确答案是 DPMT:t.run() 就是一次普通的方法调用,在 main 线程里同步执行,DP 打印完才轮到 MT。MTDP 只有在换成 t.start() 时才可能出现——而且只是「可能」,因为两个线程的调度顺序不确定。笔者用 JDK 17 实际跑了一遍,输出确定为 DPMT。
run() 只是普通方法:定义「做什么」。
Java 官方文档(Thread 类 Javadoc)对 run() 的定义平淡无奇:如果线程构造时传入了 Runnable 对象,就调用它的 run 方法,否则什么都不做。它就是一个普通的实例方法,没有任何线程语义。直接 t.run(),方法体压在当前调用线程的栈上同步执行,跑完才返回,全程没有新线程诞生。把它当成「启动线程的另一种写法」,就是这题要筛掉的错误直觉。
start() 才是开线程:决定「在哪个线程做」。
Javadoc 对 start() 的描述则是:使这个线程开始执行,由 Java 虚拟机调用该线程的 run 方法,结果是两个线程并发运行——当前线程从 start 调用返回继续向下走,新线程执行 run。完整链路是:调用 start() → JVM 创建线程 → 状态从 NEW 转为 RUNNABLE → 等待 CPU 调度 → 回调 run() 方法体。直接调 run() 则跳过中间全部步骤。
写个对照实验把语义钉死(JDK 17,javac RunVsStart.java && java RunVsStart):让 run 方法体打印当前线程名,分别用两种方式调用:
Thread t1 = new Thread(() ->
System.out.println("t.run() 内执行线程: " + Thread.currentThread().getName()));
t1.run();
Thread t2 = new Thread(() ->
System.out.println("t.start() 后 run() 执行线程: " + Thread.currentThread().getName()));
t2.start();
t2.join();
try { t2.start(); }
catch (IllegalThreadStateException e) {
System.out.println("重复 start() 抛出: " + e.getClass().getSimpleName());
}
t1.run(); // run() 可反复调
实测输出:
t.run() 内执行线程: main
t.start() 后 run() 执行线程: Thread-1
重复 start() 抛出: IllegalThreadStateException
t.run() 内执行线程: main
t.run() 打印 main——方法跑在调用者线程里;t.start() 打印 Thread-1——JVM 新建的线程在执行(编号按 Thread 对象的创建顺序分配,t1 是 Thread-0,所以 t2 是 Thread-1)。输出差异就是执行线程的差异。
两个附加陷阱,追问必考。
其一,start() 是一次性的。Javadoc 写得很直白:一个线程启动超过一次永远不合法,执行完毕后也不能重启,重复调用抛 IllegalThreadStateException——上面的实测第三行就是证据。其二,run() 想调几次调几次,但它永远是调用者线程里的同步普通方法,调一百次也不会产生任何并发。想复用逻辑又要并发,正确姿势是重写 run 或传入 Runnable,然后调 start()。
图 2:对照实验、start() 内部链路与两个易错点。
答题一句话收尾。
run() 定义「做什么」,start() 决定「在哪个线程做」——想并发,调 start()。笔试遇到代码输出题,先问自己 run 方法体跑在哪个线程上,顺序题就迎刃而解。你还见过哪些「看着像开线程其实不是」的写法?欢迎补充。