连载中 2/21

线程的六种状态:start 和 run 根本不是一回事

2026-10-23 · 73 阅读 · 0 评论 · 0 赞

调用 run() 的代码,根本没开线程

新人翻车现场:想让任务异步执行,写了个 new Thread(task).run()——程序「正常」跑完,却慢得和没加线程一样。原因很简单:start() 才会请求操作系统创建新线程,而 run() 只是一个普通方法调用,任务在当前线程里同步执行。一行输出就能拆穿它:

public class StartVsRun {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> System.out.println("执行者:" + Thread.currentThread().getName());
        Thread t = new Thread(task);
        t.run();    // 输出:执行者:main —— 主线程自己同步跑的
        t.start();  // 输出:执行者:Thread-0 —— 这才是真并发
        t.join();
    }
}

顺便记一条铁律:start() 对同一个线程只能调一次。线程终止后再 start 会抛 IllegalThreadStateException——线程不可复生,要跑新任务就 new 一个新线程,或者用第 14 篇讲的线程池来复用。

三种创建姿势:无返回、有返回、别继承

创建线程有三条路,选型一句话:任务抽象用 Runnable,要返回值用 Callable,继承 Thread 基本不用——Java 单继承,一个 extends 占死继承位,而且任务和执行本来就不该绑在一起。

// 1. Runnable:无返回值的任务标准
Runnable r = () -> System.out.println("跑起来");
new Thread(r).start();

// 2. Callable + FutureTask:有返回值、能抛受检异常
Callable<Integer> c = () -> {
    Thread.sleep(100);
    return 66;
};
FutureTask<Integer> ft = new FutureTask<>(c);
new Thread(ft).start();
System.out.println(ft.get());  // 阻塞等结果,拿到 66

FutureTask 的 get() 是阻塞的——这里埋个伏笔:生产环境不会一个任务开一个线程,而是统一交给线程池调度,Callable 直接作为池的入参。线程池的完整故事在第 14、15 篇展开。

六种状态:一张流转图记牢

Thread.State 枚举只有六个值:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。先说第一个反直觉:没有 RUNNING 状态——就绪和运行中统一叫 RUNNABLE,因为「是否真的在 CPU 上跑」是操作系统调度器的视角,Java 层感知不到。流转关系一张图:

NEW ──start()──→ RUNNABLE ──run() 执行完──→ TERMINATED

RUNNABLE 的三条岔路(最终都会回来):
  → BLOCKED        在等 synchronized 的锁,拿到锁就回
  → WAITING        wait() / join() / park(),等 notify() / 目标线程结束 / unpark()
  → TIMED_WAITING  sleep(n) / wait(n) / join(n),到点或被唤醒就回

BLOCKED 和 WAITING 的区别值得单独强调:BLOCKED 在等锁,由 JVM 仲裁谁先进;WAITING 在等通知,人已经出了 synchronized 块把锁让了出去,纯粹等人喊。jstack 里看到大量 BLOCKED,第一反应查锁竞争;看到大量 WAITING,查该 notify 的那个家伙是不是没 notify。

第二个反直觉:阻塞 I/O 也算 RUNNABLE

socket 读不到数据、线程卡在 read() 上,状态仍然是 RUNNABLE。因为 Thread.State 只描述 JVM 层面与 monitor 相关的状态,操作系统层面的网络等待它一概不知。jstack 排障时看到几十个 RUNNABLE 的线程,别急着说「它们都在跑」——先看栈帧是不是停在 socketRead、epollWait 这类本地方法上。记住这句话:状态要和线程栈一起看,状态只回答「在等锁还是在等通知」,栈才回答「具体卡在哪一行」。

interrupt:不是停止,是商量

很多人以为 thread.interrupt() 能停线程,错。interrupt 只是把中断标志置为 true,行为分三种情况:目标线程正在 sleep/wait/join 中阻塞——被唤醒并抛 InterruptedException,同时标志位被清空;正在 RUNNABLE 正常跑——什么都不抛,需要代码自己定期检查 isInterrupted() 决定要不要收工;目标线程已 TERMINATED——静默无效。线程要不要停,决定权始终在自己手里,这叫协作式中断。为什么设计得这么客气?因为强制杀死线程可能导致它持有的锁不释放、写到一半的数据不完整——被废弃的 Thread.stop() 就是反面教材。

地基打完,下一篇进入本系列第一件兵器:synchronized——它到底锁的是什么,字节码里的 monitorenter 和 monitorexit 又藏了什么。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Java#线程#线程状态#JUC#并发编程

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10113 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9876 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9306 阅读 · 21 评论 · 287 赞