线程基础

上一章我们搭好了并发的世界观——进程、线程、并发、并行。这一章开始动手:怎么在 Java 里真正”造一个线程出来”。

你可能会觉得 new Thread().start() 能有多难?但真实情况是:很多人写了 5 年 Java 都没搞清楚 start()run() 的区别、不知道 interrupt 不是强制停止、不理解为什么 join 能让线程”排队”。这些细节决定了你写的是”能跑的并发代码”还是”会出事的并发代码”。

一、创建线程的三种方式

Java 给了我们三种”开线程”的方法,从简单到强大依次递进。

1.1 方式一:继承 Thread 类

最直接的方式——继承 Thread,重写 run() 方法。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 在跑");
    }
}

MyThread t = new MyThread();
t.start();   // 不要调 run(),要调 start()!

关键点

  • start()创建新的 OS 线程并调用 run();直接调 run() 只是普通方法调用,不会开新线程——这是新手最常犯的错。
  • Java 单继承,继承了 Thread 就不能继承别的类——这种方式不够灵活,实际项目里不推荐

1.2 方式二:实现 Runnable 接口

更推荐的方式——实现 Runnable,把它丢给 Thread 执行。

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("任务跑在 " + Thread.currentThread().getName());
    }
}

Thread t = new Thread(new MyTask(), "worker-1");
t.start();

// 或者用 Lambda(Runnable 是函数式接口)
new Thread(() -> System.out.println("Lambda 也在 " + Thread.currentThread().getName())).start();

优势

  • Runnable 是接口,可以同时继承别的类。
  • 任务(Runnable)和执行(Thread)解耦——同一个 Runnable 可以丢给线程池、虚拟线程、ForkJoinPool
  • Runnable 是函数式接口,可以用 Lambda。

Thread 本身也实现了 Runnable——Thread 类内部有个 private Runnable target; 字段,Thread.run() 默认实现就是 if (target != null) target.run();。所以继承 Thread 重写 run,本质是覆盖了”调用 target”的默认行为。

1.3 方式三:实现 Callable + Future

Runnablerun() 返回 void——任务跑完拿不到结果。如果你需要”开线程算个值,最后取回来”,用 Callable

import java.util.concurrent.*;

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};

FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();

Integer result = future.get();   // 阻塞直到任务完成
System.out.println("结果是 " + result);

关键点

  • Callable<V>call() 方法有返回值,且能抛受检异常Runnable.run() 不行)。
  • FutureTaskFutureRunnable 的桥梁——它可以丢给 Thread 或线程池执行。
  • future.get()阻塞直到任务完成——这是 Future 的局限,第 44 章的 CompletableFuture 解决了这个问题。

实际项目里 Callable 几乎总是配合 ExecutorService.submit() 使用——直接 new ThreadFutureTask 的场景很少。

1.4 三种方式对比

方式返回值异常推荐度
继承 Thread不能抛不推荐(单继承局限)
实现 Runnable不能抛推荐(简单任务)
实现 Callable能抛推荐(需要结果时)

二、线程的生命周期:六种状态

一个线程从生到死,要经历若干状态。Java 把它们定义在 Thread.State 枚举里——共 6 种

        ┌──────────────────────────────────────────┐
        │                                          ▼
   ┌─NEW───→ RUNNABLE ──┬──→ BLOCKED ──┬──→ RUNNABLE
   │                     │              │
   │                     ├──→ WAITING ──┤
   │                     │              │
   │                     └──→ TIMED_WAITING ──┘

   └─────────────────────────────────────────────→ TERMINATED

2.1 六种状态详解

状态含义进入方式
NEW已创建但未 startnew Thread()
RUNNABLE可运行(可能在跑也可能在等 CPU)start()
BLOCKED等待 monitor 锁(synchronized)进入 synchronized 块时锁被占
WAITING无限期等待被唤醒wait() / join() / LockSupport.park()
TIMED_WAITING限时等待sleep(ms) / wait(ms) / join(ms) / parkNanos
TERMINATED线程执行结束run() 正常返回或抛未捕获异常

注意:Java 的 RUNNABLE 把”正在跑”和”等待 CPU 时间片”合并了——操作系统层面这两者叫 Running 和 Ready,但 Java 不区分。也就是说,一个 Java 线程在 RUNNABLE 状态下可能在跑,也可能在排队等 CPU

2.2 BLOCKED vs WAITING 的区别

新手最容易混淆这两个状态——都是”等着”,但等待的东西不同:

  • BLOCKED:等的是 synchronized 锁。线程想进入 synchronized 块/方法,但锁被别的线程持有,于是被放进”锁池”等待。当锁释放时,JVM 会从锁池里挑一个 BLOCKED 线程唤醒。
  • WAITING:等的是某个事件/通知。比如调用了 obj.wait()(等其他线程 obj.notify())、thread.join()(等目标线程结束)、LockSupport.park()(等 unpark)。

重要ReentrantLock.lock() 让线程进入的是 WAITING(更准确说是 park),不是 BLOCKED!只有 synchronized 才会让线程进入 BLOCKED 状态。这是 JUC 锁和 synchronized 在状态机上的细微差别。

三、线程的核心方法

3.1 start() vs run()

start()  → JVM 创建新线程 → 新线程里调用 run()
run()    → 普通方法调用,在当前线程里跑,不开新线程

无数 bug 的源头就是把 start() 写成了 run()——代码”看起来跑了”,但其实是单线程的。

3.2 sleep(ms)——睡眠

Thread.sleep(ms)当前线程睡眠指定毫秒,不释放锁

Thread.sleep(1000);              // 睡 1 秒
TimeUnit.SECONDS.sleep(1);       // 更可读的写法

sleep 期间线程进入 TIMED_WAITING。如果中途被 interrupt,会抛 InterruptedException——所以 sleep 是可中断的。

3.3 join()——等待另一个线程结束

thread.join()当前线程阻塞,直到 thread 结束。

Thread t = new Thread(() -> { /* ... */ });
t.start();
t.join();        // 等 t 结束才继续
t.join(1000);    // 最多等 1 秒,超时继续

join 的本质是当前线程调用了 t.wait()——所以 join 会让当前线程进入 WAITING,并且会响应中断

3.4 yield()——礼让

Thread.yield() 是个”hint”——告诉调度器”我愿意让出 CPU”。但调度器完全可以无视——你可能 yield 之后立刻又拿到 CPU。

Thread.yield();   // 礼让,但不保证真的让

实战中 yield 几乎不用——它没有可靠的语义。需要”等待某条件成立”应该用 wait/notifyCondition,而不是 yield 死循环。

3.5 interrupt()——中断

Java 没有”强制停止线程”的安全方法——Thread.stop() 早就废弃了(强制停止可能让对象处于不一致状态)。中断是 Java 提供的”协作式停止”机制。

Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 干活
    }
});
t.start();
t.interrupt();   // 设置中断标志

关键点

  • interrupt() 只是设置中断标志,不强制停止。
  • 线程应该主动检查 Thread.currentThread().isInterrupted() 并优雅退出。
  • 如果线程在 sleep/wait/join 时被 interrupt,会抛 InterruptedException同时清除中断标志
  • 捕获 InterruptedException 后通常要么重新设置中断标志(Thread.currentThread().interrupt()),要么向上抛——不要吞掉异常

3.6 方法速查表

方法作用是否释放锁是否响应中断
start()启动新线程
run()任务体(直接调是普通调用)
sleep(ms)当前线程睡眠❌ 不释放✅ 抛异常
join()等待目标线程结束✅ 释放(内部用 wait)✅ 抛异常
yield()礼让 CPU(hint)❌ 不释放
interrupt()设置中断标志
isInterrupted()查询中断标志

四、守护线程(Daemon Thread)

Java 的线程分两类:用户线程(User Thread)和守护线程(Daemon Thread)。

  • 用户线程:默认都是用户线程。JVM 会等所有用户线程结束才退出。
  • 守护线程:在后台默默干活的线程。JVM 不会等守护线程结束——最后一个用户线程结束,JVM 直接退出,所有守护线程被强制终止。

典型的守护线程:GC 线程Finalizer 线程编译器线程——它们都是 JVM 的”后勤人员”。

Thread t = new Thread(() -> {
    while (true) {
        // 定期清理工作
    }
});
t.setDaemon(true);   // 必须在 start() 之前设置
t.start();

关键点

  • setDaemon(true) 必须在 start() 之前调用,否则抛 IllegalThreadStateException
  • 守护线程里不要操作不可恢复的资源(写文件、改数据库)——因为它可能被任意时刻终止,资源未关闭就消失了。
  • 守护线程创建的子线程默认是守护线程

五、线程优先级:不保证顺序

Thread 有个 setPriority(int) 方法,范围 1(MIN_PRIORITY)到 10(MAX_PRIORITY),默认 5(NORM_PRIORITY)。

t.setPriority(Thread.MAX_PRIORITY);   // 10

但要注意——优先级只是给操作系统的 hint,不保证高优先级线程先执行。在不同 OS、不同 JVM 实现上行为完全不同。永远不要靠优先级来保证正确性——它最多用来”微调性能”,而且效果难以预测。

六、实战:多线程模拟并发下载

下面这个例子演示三种创建线程的方式、join 等待、interrupt 中断、守护线程,以及完整的生命周期观察。

Java · 在线运行

观察重点

  • t1 在 start 前是 NEW,start 后变 RUNNABLE,结束后变 TERMINATED——这是状态机的核心流转。
  • join 让主线程等待子线程——主线程打印”等待下载完成”后阻塞,直到 t1/t2/t3 都结束才继续。
  • future.get() 阻塞取 Callable 返回值——Callable 适合”开线程算个结果”。
  • monitor.interrupt() 不会强制停止线程——它只是设置标志,监控线程的 sleep 抛出 InterruptedException,循环检查到中断后 break 退出。
  • 守护线程 monitor 即使是 while(true),主线程一结束它也会跟着退出——这就是守护线程的意义。
  • 优先级高的线程不一定先跑——输出顺序与优先级无必然关系。

七、本章小结

主题核心要点
三种创建方式继承 Thread(不推荐)/ 实现 Runnable(推荐)/ Callable+Future(需返回值)
start vs runstart 开新线程,run 是普通方法调用
六种状态NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED
BLOCKED vs WAITINGBLOCKED 等 synchronized 锁,WAITING 等通知/事件
sleep不释放锁,响应中断
join等待目标线程结束(内部用 wait),释放锁,响应中断
yield礼让 hint,无可靠语义
interrupt协作式停止——只设标志,线程自己检查
守护线程setDaemon(true) 必须在 start 前;JVM 不等守护线程退出
优先级只是 hint,不保证顺序,不要靠它保证正确性

记忆口诀

  • start 开线程,run 是普通方法——别再搞错。
  • sleep 不放锁,join 会放锁——这是面试高频题。
  • BLOCKED 等 synchronized,WAITING 等通知——只有 synchronized 让线程 BLOCKED。
  • interrupt 是协作不是强停——线程自己决定怎么响应。
  • 守护线程是后勤——别让它写关键数据。
  • 优先级靠不住——别拿它当调度保证。

结语:从”会开线程”到”会管线程”

这一章我们学会了”造一个线程”——三种创建方式、六种状态、核心方法。但真实生产环境里,你几乎不会直接 new Thread——而是用线程池(第 43 章)。直接 new 线程的问题在于:创建/销毁开销大、无法控制并发数、没有任务队列、没有异常处理。

不过理解 Thread 是理解一切并发的基础——线程池内部还是用 ThreadCompletableFuture 默认用 ForkJoinPool,虚拟线程本质也是 Thread 的子类。把这些底子打牢,后面才能游刃有余。

下一章我们直面并发最核心的问题——线程安全:为什么多个线程同时改一个变量会出问题?什么是竞态条件?为什么 i++ 不是原子操作?这将是真正”并发入坑”的一章。