调用start()才真正创建OS级线程并可能多核并行,而run()仅是主线程内普通方法调用;可通过/proc/[pid]/task/验证LWP存在与否、jstack查看RUNNABLE状态、top -H观察CPU分布来直观区分。

直接调用 run() 不会触发多核并行,它只是主线程里一个普通方法调用;而调用 start() 才真正让 JVM 向操作系统申请新内核线程,从而可能被调度到不同物理核心上运行。要直观看到这个差异,关键不是“写多复杂”,而是用可观察的系统级指标把线程和 CPU 核心绑定关系显性化。
一、用 Thread.currentThread().getId() + /proc/[pid]/task/[tid]/status 验证线程创建
Java 的 Thread.getId() 是 JVM 内部编号,不能反映 OS 线程 ID;但 Linux 下每个 Java 线程对应一个 内核线程(LWP),其真实 PID 可在 /proc/[java_pid]/task/ 目录下查到。配合 task/[tid]/status 中的 Cpus_allowed_list 字段,就能看到该线程被允许运行在哪几个逻辑核心上。
- 启动 Java 程序后,先用
ps -T -p [java_pid]获取所有线程的 LWP ID - 对每个 LWP ID,读取
/proc/[java_pid]/task/[lwp_id]/status | grep Cpus_allowed_list - 对比:start() 后的新线程通常显示允许全部核心(如
0-11),而 run() 调用不会产生新 LWP,自然查不到对应 task 目录
二、用 Runtime.getRuntime().availableProcessors() 辅助判断硬件能力
这行代码返回的是 JVM 感知到的**可用逻辑处理器数**(例如 12 表示 6 核 12 线程)。它本身不区分 start/run,但它是后续观察调度行为的前提——如果返回 1,那无论你怎么 start,也跑不出单核并发效果。
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
- 建议在 main 开头就打印:
System.out.println("Cores: " + Runtime.getRuntime().availableProcessors()); - 若输出为 1,说明当前环境(Docker 限制、WLS、某些 IDE 模拟器)禁用了多核,此时 start() 仍会新建线程,但 OS 只能把它调度到同一个核心上轮转(即并发而非并行)
三、用 jstack + top -H 实时观察线程状态与 CPU 占用分布
这是最贴近“直观演示”的方式:一边看线程是否处于 RUNNABLE 状态,一边看哪个 CPU 核心在真实吃满。
- 写一个死循环 + System.nanoTime() 计数的 run() 方法体(避免被 JIT 优化掉)
- 分别测试两种情形:
- 情形 A:用
new Thread(() -> { while(true) cnt++; }).start(); - 情形 B:用
new Thread(() -> { while(true) cnt++; }).run();
- 情形 A:用
- 运行中执行
jstack [pid] | grep java.lang.Thread.State,A 中能看到多个 RUNNABLE 线程;B 中只有 main 是 RUNNABLE - 同时开另一个终端运行
top -H -p [pid],按1显示各核负载 —— A 中大概率出现多个线程分散在不同 CPU% 列高亮,B 中只有 %CPU 集中在单个核心上跳动
四、加一点“核亲和力”佐证:用 jcmd 设置线程绑定(可选进阶)
从 JDK 10 起,jcmd [pid] VM.native_memory summary 和第三方工具如 JDK Mission Control 可配合 taskset 做验证。更直接的是:
- 用
taskset -c 0,1 java YourApp启动程序,限定只用核心 0 和 1 - 再用
jstack查线程,start() 创建的线程仍会出现在/proc/[pid]/task/下,且Cpus_allowed_list显示为0-1 - 而 run() 调用的线程永远不会出现在该目录 —— 它压根没被 OS 当作独立调度实体

















