Skip to content

线程基础与状态

1. 这是什么

线程是程序执行的最小调度单位。
学习并发的第一步,不是先看锁,而是先把线程本身的生命周期、调度方式和状态变化搞清楚。

一句话理解:

  • 进程像“运行中的应用”
  • 线程像“应用里真正执行任务的人”

2. 为什么重要

很多并发问题并不是锁本身导致的,而是对这些基础概念理解不清:

  • 线程什么时候创建
  • 线程什么时候阻塞
  • 线程什么时候等待
  • 为什么频繁创建线程代价高
  • 为什么一个看起来“没做什么”的线程,也会占资源

基础越扎实,后面看线程池、同步器、死锁分析和性能排查就越稳。

3. 先建立直觉:进程、线程、并发、并行

3.1 进程和线程的区别

可以先用一个非常实用的理解:

  • 进程:资源分配的基本单位
  • 线程:CPU 调度执行的基本单位

一个进程通常会包含:

  • 堆内存
  • 方法区
  • 打开的文件和网络资源
  • 多个线程

线程之间:

  • 共享进程内资源
  • 但也有自己的程序计数器、栈等执行上下文

3.2 并发和并行的区别

  • 并发:一段时间内“看起来一起做”,本质可能是快速切换
  • 并行:同一时刻真的同时执行

在单核 CPU 上:

  • 更常见的是并发,不是真并行

在多核 CPU 上:

  • 既可能并发,也可能并行

工程上更重要的是:

  • 你写并发程序时,重点不是纠结字面区别
  • 而是理解多个执行流会互相影响、共享数据、争夺 CPU 和资源

4. 核心内容

4.1 Java 创建线程的常见方式

最常见的方式有三类:

  • 继承 Thread
  • 实现 Runnable
  • 实现 Callable 后配合 FutureTask 或线程池

现在更推荐的理解方式是:

  • 任务和线程分离
  • Runnable / Callable 描述任务
  • 用线程或线程池执行任务

4.2 用户线程和守护线程

Java 线程分为:

  • 用户线程
  • 守护线程

区别是:

  • 用户线程决定 JVM 是否继续存活
  • 当只剩守护线程时,JVM 可以退出

典型守护线程例子:

  • 垃圾回收相关线程

工程里要记住:

  • 守护线程适合辅助性工作
  • 不适合承载必须完成的核心业务逻辑

4.3 Java 线程状态有哪些

Java 里的线程状态主要有 6 种:

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING
  • TIMED_WAITING
  • TERMINATED

最重要的是不要死背名字,而要理解“它为什么进入这个状态”。

下面用一张状态流转图,把“线程是怎么从一个状态走到另一个状态的”串起来看:

可以这样理解这张图:

  • NEW -> RUNNABLE:线程对象创建后,只有调用 start(),线程才真正进入可调度状态。
  • RUNNABLE -> BLOCKED:这是“抢锁失败”,只和 synchronized 监视器锁竞争有关。
  • RUNNABLE -> WAITING / TIMED_WAITING:这是线程主动进入等待,只是一个无限期等待,一个带超时等待。
  • 这张图按“概念理解”做了合并表达:等待条件满足后统一画回 RUNNABLE,表示线程重新回到可调度状态。
  • 如果你从底层实现去看,wait() / join() 被唤醒后还要重新拿到监视器锁,这个过程可以理解为会短暂经过锁竞争,但在入门状态图里通常不单独展开。
  • RUNNABLE -> TERMINATED:线程执行结束后进入终止状态,不能再次 start()

4.4 每个状态怎么理解

NEW

  • 线程对象刚创建
  • 但还没有调用 start()

RUNNABLE

  • 线程已经具备运行资格
  • 可能正在运行,也可能正在等待 CPU 时间片

Java 把“就绪”和“运行中”统一归到 RUNNABLE

BLOCKED

  • 线程正在等待进入 synchronized 监视器锁
  • 也就是“想进同步块,但锁被别人占着”

WAITING

  • 线程进入无限期等待,等别人明确唤醒
  • 常见来源:Object.wait()Thread.join()LockSupport.park()

TIMED_WAITING

  • 线程进入带超时的等待
  • 常见来源:sleep()wait(timeout)join(timeout)

TERMINATED

  • 线程执行结束

4.5 sleepwaitjoin 到底有什么区别

这是线程基础里最容易混淆的一组。

方法属于谁会不会释放锁典型作用
Thread.sleepThread不会让当前线程暂停一段时间
Object.waitObject当前线程等待被唤醒,常配合同步块
Thread.joinThread不用于协调锁释放等待另一个线程执行完

最常见误区:

  • sleep 不会释放已经持有的监视器锁
  • wait 必须在持有对象监视器的同步块中调用

4.6 为什么频繁创建线程代价高

线程不是“随便 new 一下就没成本”的轻量对象。
它的成本包括:

  • 线程栈内存
  • 操作系统线程创建和销毁开销
  • 调度开销
  • 上下文切换开销

这也是为什么生产环境通常不建议:

  • 来一个任务就 new Thread(...)

而更推荐:

  • 用线程池复用线程资源

4.7 上下文切换为什么会影响性能

当 CPU 从一个线程切到另一个线程时,需要保存和恢复执行现场,例如:

  • 程序计数器
  • 寄存器状态
  • 栈相关上下文

如果线程过多、切换过于频繁,就会出现:

  • CPU 真正干活时间变少
  • 系统更多时间花在“切换”而不是“执行”上

5. 学习重点

这一章真正要掌握的是:

  • 线程是执行单位,不是资源容器
  • 线程状态要和具体行为对应起来理解
  • BLOCKEDWAITINGTIMED_WAITING 是三个不同语义
  • sleepwaitjoin 的用途和效果不同
  • 频繁创建线程有真实资源代价

6. 常见问题

6.1 把线程和进程混为一谈

这会导致你后面对:

  • 资源共享
  • 线程安全
  • 内存模型

的理解都混乱。

6.2 不清楚线程何时进入阻塞或等待

例如很多人会把:

  • BLOCKED
  • WAITING
  • TIMED_WAITING

统称为“卡住了”,但它们背后的原因完全不同。

6.3 任务一多就直接 new Thread

这在简单 demo 里没问题,但在生产环境里通常不合适。
线程数量失控会带来明显的调度和资源压力。

6.4 误以为 sleep 会释放锁

不会。
线程 sleep 时,如果它已经拿到了 synchronized 锁,别人仍然进不来。

7. 动手验证

这一节可以直接复制运行,边看边验证。

7.1 准备一个可运行示例

新建文件 ThreadStateDemo.java,内容如下:

java
public class ThreadStateDemo {
    private static void waitForState(Thread thread, Thread.State expected) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            if (thread.getState() == expected) {
                return;
            }
            Thread.sleep(20);
        }
    }

    public static void main(String[] args) throws Exception {
        final Object lock = new Object();
        final Object monitor = new Object();

        Thread newThread = new Thread(() -> {}, "new-thread");
        System.out.println("newState=" + newThread.getState());

        Thread timedWaitingThread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "timed-waiting-thread");
        timedWaitingThread.start();
        waitForState(timedWaitingThread, Thread.State.TIMED_WAITING);
        System.out.println("timedWaitingState=" + timedWaitingThread.getState());

        Thread lockHolder = new Thread(() -> {
            synchronized (lock) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "lock-holder");

        Thread blockedThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("blockedThreadEnteredLock");
            }
        }, "blocked-thread");

        lockHolder.start();
        Thread.sleep(100);
        blockedThread.start();
        waitForState(blockedThread, Thread.State.BLOCKED);
        System.out.println("blockedState=" + blockedThread.getState());

        Thread waitingThread = new Thread(() -> {
            synchronized (monitor) {
                try {
                    monitor.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "waiting-thread");
        waitingThread.start();
        waitForState(waitingThread, Thread.State.WAITING);
        System.out.println("waitingState=" + waitingThread.getState());

        Thread worker = new Thread(() -> {
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "worker");

        Thread joiner = new Thread(() -> {
            try {
                worker.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "joiner");

        worker.start();
        joiner.start();
        waitForState(joiner, Thread.State.WAITING);
        System.out.println("joinWaitingState=" + joiner.getState());

        Thread daemonThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "daemon-thread");
        daemonThread.setDaemon(true);
        daemonThread.start();
        System.out.println("daemonFlag=" + daemonThread.isDaemon());

        synchronized (monitor) {
            monitor.notifyAll();
        }

        timedWaitingThread.join();
        lockHolder.join();
        blockedThread.join();
        waitingThread.join();
        worker.join();
        joiner.join();

        System.out.println("terminatedState=" + joiner.getState());
    }
}

7.2 编译并运行

bash
javac ThreadStateDemo.java
java ThreadStateDemo

如果你在 PowerShell 中操作,也直接执行同样两条命令即可。

7.3 你应该观察到什么

输出不一定逐字完全一致,但应包含这些关键信息:

text
newState=NEW
timedWaitingState=TIMED_WAITING
blockedState=BLOCKED
waitingState=WAITING
joinWaitingState=WAITING
daemonFlag=true
terminatedState=TERMINATED

中间还可能出现:

text
blockedThreadEnteredLock

7.4 每一行在验证什么

  • newState=NEW:说明线程对象创建后、调用 start() 前处于 NEW
  • timedWaitingState=TIMED_WAITING:说明 sleep 会让线程进入超时等待
  • blockedState=BLOCKED:说明线程在等待进入 synchronized 锁时会阻塞
  • waitingState=WAITING:说明 wait() 会让线程进入无限等待
  • joinWaitingState=WAITING:说明 join() 的本质也是等待另一个线程结束
  • daemonFlag=true:说明该线程被设置成守护线程
  • terminatedState=TERMINATED:说明线程执行完后进入终止状态

7.5 再做两个延伸验证

你可以继续做下面两个实验:

  1. lockHolder 里把 Thread.sleep(1000) 改得更短或更长
  2. waitingThread 中的 monitor.wait() 改成 monitor.wait(500)

你可以观察:

  • BLOCKED 状态持续时间会变化
  • WAITING 会变成 TIMED_WAITING

8. 练习建议

下面这些练习做完,这一章会更扎实:

  • 写一个 demo 对比 sleepwaitjoin 的行为差异
  • 观察线程从 NEWTERMINATED 的关键状态变化
  • 创建一批短生命周期线程,体会频繁创建线程的资源代价
  • 分析一个业务线程为什么进入 BLOCKEDWAITING

9. 自测问题

  • Java 线程常见状态有哪些?
  • BLOCKEDWAITING 的核心区别是什么?
  • 守护线程和用户线程有什么区别?
  • 为什么生产环境通常不建议频繁手动创建线程?
  • sleepwaitjoin 各自适合什么场景?

10. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • 线程是执行单位,进程是资源容器
  • RUNNABLE 在 Java 里同时包含就绪和运行中的语义
  • BLOCKED 主要对应监视器锁竞争
  • WAITING / TIMED_WAITING 体现的是等待语义
  • sleep 不释放锁,wait 需要在同步块中使用
  • 守护线程适合辅助任务,不适合承载必须完成的核心逻辑