Thread 常用 API 与线程执行特征 | JavaSE

Thread 常用 API 与线程执行特征

一、学习目标

完成本章后,你应该能够:

  • 熟练使用 getName()setName()currentThread() 获取和设置线程信息。
  • 理解 Thread.sleep() 的真实含义,知道究竟是哪一个线程进入休眠。
  • 理解 join() 的真实语义,能够让当前线程等待指定线程执行结束。
  • 理解线程执行顺序具有不确定性,不能通过一次运行结果推断固定调度规律。
  • 了解线程优先级、守护线程、yield()interrupt() 等常见控制能力。
  • 理解线程启动、运行、等待与结束之间的大致状态变化。
  • 能够通过线程名称和简单 API 调试多线程程序。

二、核心知识

2.1 Thread 不只是用来创建线程

前面已经学习:

Thread thread = new Thread(task);
thread.start();

但是 Thread 不只是:

“创建并启动线程的类”。

它还负责描述和控制线程的一系列属性与行为,例如:

线程叫什么名字?
当前代码是谁在执行?
线程是否还活着?
线程能不能暂停一段时间?
当前线程能不能等待另一个线程结束?
线程是不是守护线程?
线程有没有收到中断请求?

因此进入真正的多线程开发之后,需要掌握一组常用 Thread API。


2.2 本阶段常用 Thread API

| API | 作用 | | ---------------------- | ---------------------------------- | | getName() | 获取线程名称 | | setName(String name) | 设置线程名称 | | currentThread() | 获取当前正在执行代码的线程 | | sleep(long millis) | 让当前线程休眠指定时间 | | join() | 等待指定线程终止 | | isAlive() | 判断线程是否还处于活动状态 | | getPriority() | 获取线程优先级 | | setPriority(int) | 设置平台线程优先级 | | setDaemon(boolean) | 设置平台线程是否为守护线程 | | isDaemon() | 判断线程是否为守护线程 | | interrupt() | 向线程发送中断请求 | | isInterrupted() | 查询线程是否已收到中断 | | yield() | 向调度器提示当前线程愿意让出处理器 |

其中最需要熟练掌握的是:

线程名称
currentThread()
sleep()
join()

其他 API 先建立正确认识,不建议现阶段依赖它们设计复杂业务。


三、使用方法

3.1 getName():获取线程名称

每个 Thread 对象都有名称。

例如:

public class ThreadNameDemo {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            System.out.println("任务执行");
        });

        System.out.println(t1.getName());

        t1.start();
    }
}

如果没有主动设置名称,传统平台线程通常会获得类似:

Thread-0
Thread-1
Thread-2

这样的自动名称。

线程名称本身不会改变程序执行逻辑,但是对于:

日志
Debug
异常分析
并发问题排查

非常重要。

如果输出只有:

开始执行
执行完成

出现问题时很难知道:

到底是哪条线程执行的?

如果输出:

订单线程-1:开始执行
订单线程-2:开始执行

定位问题就容易很多。


3.2 setName():设置线程名称

可以:

Thread t1 = new Thread(() -> {
    System.out.println("执行任务");
});

t1.setName("下载线程");

t1.start();

然后:

System.out.println(t1.getName());

得到:

下载线程

更常见的做法是创建 Thread 时直接指定名称:

Thread t1 = new Thread(
        () -> System.out.println("下载"),
        "下载线程"
);

t1.start();

如果继承 Thread:

class MyThread extends Thread {

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(getName());
    }
}

创建:

MyThread t1 = new MyThread("1号线程");
MyThread t2 = new MyThread("2号线程");

t1.start();
t2.start();

3.3 currentThread():到底是谁正在执行这段代码

非常重要:

Thread.currentThread()

返回:

当前正在执行这句代码的 Thread 对象。

例如:

public class CurrentThreadDemo {

    public static void main(String[] args) {

        Thread current =
                Thread.currentThread();

        System.out.println(
                current.getName()
        );
    }
}

在普通 Java 程序中通常输出:

main

因为:

main()

正在由 main 线程执行。


3.4 currentThread() 在子线程中的效果

public class CurrentThreadDemo {

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {

            Thread current =
                    Thread.currentThread();

            System.out.println(
                    current.getName()
            );

        }, "文件下载线程");

        t1.start();
    }
}

这里:

Thread.currentThread()

得到的是:

文件下载线程

而不是 main。

所以一定记住:

谁执行 currentThread(),得到的就是谁。


3.5 currentThread() 的典型应用:日志

例如:

class DownloadTask implements Runnable {

    @Override
    public void run() {

        String name =
                Thread.currentThread()
                      .getName();

        System.out.println(
                name + ":开始下载"
        );

        System.out.println(
                name + ":下载完成"
        );
    }
}

启动:

new Thread(
        new DownloadTask(),
        "下载线程-A"
).start();

new Thread(
        new DownloadTask(),
        "下载线程-B"
).start();

日志可能:

下载线程-A:开始下载
下载线程-B:开始下载
下载线程-A:下载完成
下载线程-B:下载完成

这比单纯输出:

开始下载
下载完成

有价值得多。


3.6 sleep():让当前线程休眠

Thread 提供:

Thread.sleep(long millis)

例如:

Thread.sleep(1000);

表示:

让当前正在执行这句代码的线程暂时停止执行大约 1000 毫秒。

也就是:

1000 ms = 1 s

例如:

public class SleepDemo {

    public static void main(String[] args)
            throws InterruptedException {

        for (int i = 1; i <= 5; i++) {

            System.out.println(i);

            Thread.sleep(1000);
        }
    }
}

效果大致为:

1
等待约1秒
2
等待约1秒
3
...

3.7 sleep() 到底让谁睡?

这是必考问题。

看:

t1.sleep(1000);

虽然通过 t1 写出来在语法上可以编译,但 sleep() 是:

static

静态方法。

真正的语义并不是:

让 t1 睡觉。

而是:

让当前执行 sleep() 的线程休眠。

因此工程代码应该直接写:

Thread.sleep(1000);

而不是:

t1.sleep(1000);

避免产生错误理解。

例如:

public class SleepDemo {

    public static void main(String[] args)
            throws InterruptedException {

        Thread t1 = new Thread(() -> {
            System.out.println("子线程");
        });

        t1.start();

        Thread.sleep(3000);

        System.out.println("main继续");
    }
}

休眠的是:

main线程

因为:

Thread.sleep(3000)

是 main 线程执行的。


3.8 sleep() 为什么需要处理 InterruptedException

方法:

Thread.sleep(...)

可能抛出:

InterruptedException

因此:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

或者在简单教学程序中:

public static void main(String[] args)
        throws InterruptedException {
}

都可以。

InterruptedException 的意义是:

当前正在休眠、等待等状态中的线程收到了中断请求。

中断并不是简单粗暴地“杀死线程”。

后面会进一步解释。


3.9 join():等待指定线程结束

假设:

Thread t1 = new Thread(() -> {

    for (int i = 1; i <= 5; i++) {
        System.out.println("t1:" + i);
    }
});

t1.start();

main 线程希望:

必须等 t1 全部执行完毕以后,我才能继续。

可以:

t1.join();

完整示例:

public class JoinDemo {

    public static void main(String[] args)
            throws InterruptedException {

        Thread t1 = new Thread(() -> {

            for (int i = 1; i <= 5; i++) {
                System.out.println("t1:" + i);
            }

        }, "线程A");

        t1.start();

        t1.join();

        System.out.println(
                "main:线程A已经结束"
        );
    }
}

执行模型:

main线程

t1.start()
    │
    ├──────────────→ t1执行
    │                   │
t1.join()               │
    │                   │
    │ main等待           │
    │                   │
    │               t1执行结束
    │                   │
    ←───────────────────┘
    │
main继续

所以:

t1.join() 的准确含义是:当前线程等待 t1 终止。

如果是 main 调用:

t1.join();

那么:

main等待t1

如果是 t2 调用:

t1.join();

则:

t2等待t1

3.10 join() 不是“提高线程优先级”

初学时经常把 join 称为:

线程插队

这个比喻可以帮助记忆现象,但并不准确。

例如:

t1.start();
t1.join();

不是:

把 t1 的优先级提升到最高

也不是:

强迫 CPU 只运行 t1

真正发生的是:

当前线程等待 t1 结束

系统中的其他线程仍然可以正常被调度执行。


3.11 join(long millis):最多等待一段时间

还可以:

t1.join(3000);

表示:

当前线程最多等待 t1 约 3000ms。

两种情况都可能结束等待:

情况一:
t1提前结束
→ join提前返回

情况二:
等待时间达到3000ms
→ join返回

所以它不是保证:

t1一定执行完成

而只是:

最多等待这么久

3.12 isAlive():线程是否还活着

可以:

thread.isAlive()

判断线程是否处于活动状态。

例如:

Thread t1 = new Thread(() -> {
    System.out.println("执行任务");
});

System.out.println(t1.isAlive());

t1.start();

System.out.println(t1.isAlive());

由于线程调度具有不确定性:

start()

之后第二次检查时:

t1可能仍然活着
也可能任务已经执行完毕

所以不能通过这种代码假设固定结果。

这恰好再次说明:

多线程程序不要通过“我觉得它应该运行到这里了”判断线程状态。


四、原理与进阶

4.1 多线程为什么每次输出顺序可能不同

例如:

Thread t1 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("A:" + i);
    }
});

Thread t2 = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        System.out.println("B:" + i);
    }
});

t1.start();
t2.start();

一次可能:

A:0
B:0
A:1
B:1
...

另一次可能:

B:0
B:1
B:2
A:0
...

因为:

start()

只是让线程具备开始执行的条件。

具体什么时候获得执行机会由运行时和操作系统调度决定。

因此:

线程调度具有不确定性。


4.2 sleep() 不保证精确时间

例如:

Thread.sleep(1000);

不能理解为:

精确到纳秒地暂停 1000ms,然后第 1000ms 的那个瞬间立即运行。

更准确地说:

线程至少暂时停止执行
等待指定时间到达后
重新具备继续运行的机会

之后什么时候真正再次得到 CPU,还与调度有关。

所以 sleep()

不是高精度计时器

4.3 sleep() 与 join() 的区别

这是非常重要的对比。

sleep

Thread.sleep(1000);

关注的是:

时间

意思:

当前线程暂停一段时间。

join

t1.join();

关注的是:

另一个线程是否结束

意思:

当前线程等待 t1 结束。

所以:

sleep → 等时间
join  → 等线程

4.4 start() 与 join() 还存在重要的线程间顺序保证

在 Java 内存模型中:

调用 t.start()

发生在:

t 中的所有动作之前

而线程 t 中完成的动作,又发生在:

其他线程成功从 t.join() 返回之前

这意味着 start()join() 不只是控制执行流程,它们也参与线程之间的可见性与顺序保证。

目前先记住:

start
↓
建立“启动前 → 新线程”的关系

线程执行
↓
join成功返回
↓
建立“线程完成 → 等待者继续”的关系

Java 内存模型会在并发进阶阶段进一步理解。


4.5 线程优先级

平台线程存在优先级:

Thread.MIN_PRIORITY   // 1
Thread.NORM_PRIORITY  // 5
Thread.MAX_PRIORITY   // 10

例如:

Thread t1 = new Thread(task);

t1.setPriority(
        Thread.MAX_PRIORITY
);

获取:

int priority =
        t1.getPriority();

但是必须特别注意:

线程优先级不能用来保证执行顺序。

不能写出这样的业务设计:

优先级10
所以一定先执行

优先级1
所以一定后执行

这是错误的。

线程调度最终由运行环境决定。

因此业务正确性绝不能依赖:

setPriority(...)

4.6 JDK 21 中虚拟线程的优先级

JDK 21 中:

虚拟线程

的优先级固定为:

Thread.NORM_PRIORITY

即:

5

对虚拟线程调用:

setPriority(...)

不会像平台线程那样改变其优先级。

这进一步说明:

优先级不是现代 Java 并发编程中应该依赖的核心控制机制。


4.7 守护线程

平台线程可以:

thread.setDaemon(true);

将线程设置成:

守护线程(Daemon Thread)

普通线程通常称为:

非守护线程

例如:

Thread daemon = new Thread(() -> {

    while (true) {
        System.out.println("后台服务");
    }

});

daemon.setDaemon(true);

daemon.start();

需要注意:

setDaemon(true)

必须在:

start()

之前完成。

不能:

thread.start();

thread.setDaemon(true);

4.8 守护线程真正意味着什么

当 JVM 中所有已经启动的:

非守护线程

全部终止后:

JVM 的关闭流程可以开始

守护线程本身不会为了“自己还没执行完”而阻止 JVM 关闭。

所以不要设计:

必须保证完成的重要保存任务

完全依赖守护线程执行结束。

例如:

最后一次转账必须落盘

就不能只想着:

“反正守护线程会慢慢保存完。”

因为 JVM 进入关闭流程时,不保证守护线程能把剩余业务完整做完。


4.9 yield():礼让只是提示

方法:

Thread.yield();

表示:

当前线程向调度器提示:我愿意暂时让出当前处理器使用机会。

但是:

调度器可以接受
也可以忽略

所以:

yield()

不能用于:

保证线程顺序
保证公平性
实现可靠同步

它通常不适合普通业务代码。


4.10 interrupt():中断不是强制杀死

可以:

thread.interrupt();

它表达的是:

向目标线程发送中断请求。

不是:

立即强制终止线程

例如线程正在:

Thread.sleep(10000);

其他线程:

thread.interrupt();

那么休眠线程可能收到:

InterruptedException

所以线程中断的核心思想更接近:

协作式取消

即:

A:
“请停止/请注意中断”

B:
收到信号后决定如何正确退出

而不是直接把线程从 CPU 上“击毙”。


4.11 不要使用 stop()、suspend()、resume() 作为现代方案

Thread 中历史上存在:

stop()
suspend()
resume()

这些 API 已被废弃,不应该作为现代 Java 并发程序的控制方式。

尤其:

stop()

可能破坏对象状态的一致性。

而:

suspend()

可能导致线程持有锁时被暂停,从而产生严重问题。

现代程序应采用:

中断机制
同步机制
并发工具类
任务取消机制

而不是强制暂停或杀死线程。


五、实践应用

5.1 下载任务的线程日志

public class DownloadDemo {

    public static void main(String[] args) {

        Runnable task = () -> {

            String name =
                    Thread.currentThread()
                          .getName();

            System.out.println(
                    name + ":开始下载"
            );

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {

                Thread.currentThread()
                      .interrupt();

                return;
            }

            System.out.println(
                    name + ":下载完成"
            );
        };

        Thread t1 =
                new Thread(
                        task,
                        "图片下载线程"
                );

        Thread t2 =
                new Thread(
                        task,
                        "视频下载线程"
                );

        t1.start();
        t2.start();
    }
}

5.2 主线程等待初始化任务完成

假设:

后台线程负责加载配置
main 后续业务必须依赖配置

就可以:

public class InitDemo {

    public static void main(String[] args)
            throws InterruptedException {

        Thread initThread =
                new Thread(() -> {

                    System.out.println(
                            "正在加载配置..."
                    );

                }, "配置加载线程");

        initThread.start();

        initThread.join();

        System.out.println(
                "配置加载完成,启动系统"
        );
    }
}

这里 join() 的业务含义非常明确:

没有配置
↓
后面的程序不能继续
↓
等待配置线程结束

六、常见问题

6.1 Thread.sleep() 是让指定 Thread 对象睡觉吗?

不是。

sleep() 是静态方法。

谁执行:

Thread.sleep(...)

谁进入休眠。


6.2 sleep() 到时间后一定立即继续吗?

不一定。

休眠时间结束后,线程重新具备继续运行的条件,但还要等待调度。


6.3 join() 是线程插队吗?

只能把“插队”当作入门比喻。

准确语义:

当前线程等待目标线程终止。


6.4 t1.join() 是谁在等待?

不是 t1 等。

而是:

谁调用 t1.join(),谁等待 t1。


6.5 设置最高优先级后一定最先执行吗?

不保证。

绝对不要使用线程优先级保证业务执行顺序。


6.6 setDaemon(true) 什么时候调用?

必须在:

start()

之前。


6.7 interrupt() 会立即终止线程吗?

不会。

它本质上属于:

中断请求

线程需要正确响应中断。


6.8 可以根据一次运行结果判断调度规律吗?

不可以。

例如一次结果是:

A
B
A
B

不能说明以后一定交替执行。


七、练习与验收

7.1 知识问答

  1. getName()setName() 分别做什么?
  2. currentThread() 返回谁?
  3. main 方法中调用 currentThread() 通常得到哪个线程?
  4. Thread.sleep(1000) 让谁休眠?
  5. 为什么不推荐写成 t1.sleep(1000)
  6. sleep() 到时间以后是否一定立即执行?
  7. t1.join() 的准确含义是什么?
  8. 为什么把 join() 简单理解成“插队”不够准确?
  9. sleep()join() 的核心区别是什么?
  10. 为什么线程输出顺序不固定?
  11. setPriority() 能否保证线程先执行?
  12. 平台线程优先级的大致范围是什么?
  13. 守护线程有什么执行特征?
  14. 为什么不能让重要业务仅依赖守护线程最终完成?
  15. yield() 能否保证把 CPU 让给另一个指定线程?
  16. interrupt() 是不是强制杀死线程?

7.2 代码阅读

不要运行:

public class ThreadRead {

    public static void main(String[] args)
            throws InterruptedException {

        Thread t1 = new Thread(() -> {

            System.out.println(
                    Thread.currentThread()
                          .getName()
            );

        }, "A");

        t1.start();

        t1.join();

        System.out.println(
                Thread.currentThread()
                      .getName()
        );
    }
}

回答:

  1. 子线程输出什么名称?
  2. 最后一行通常输出什么?
  3. 最后一行能否早于线程 A 的输出?
  4. 为什么?

阅读:

Thread t1 = new Thread(() -> {
    System.out.println("A");
});

t1.start();

Thread.sleep(3000);

System.out.println("B");

回答:

  1. 是 t1 睡 3 秒还是 main 睡 3 秒?
  2. "A" 是否必须等 3 秒后才能输出?
  3. "B" 为什么需要等待?

7.3 手写代码

任务一:

创建:

下载线程
日志线程

要求两个线程分别输出自己的线程名称。

任务二:

使用:

Thread.sleep(1000);

每隔大约一秒输出一个数字,共输出 5 个。

任务三:

启动线程 A,让 A 输出 1~10。

main 调用:

a.join();

确保:

main结束

只在线程 A 终止之后输出。

7.4 Debug

代码:

Thread t = new Thread(task);

t.start();

t.setDaemon(true);

判断问题并修复。


代码:

Thread t1 = new Thread(task);

t1.setPriority(10);

t1.start();

System.out.println(
        "因为优先级最高,所以t1一定执行完了"
);

指出逻辑错误。


代码:

Thread t1 = new Thread(task);

t1.start();

t1.join();

System.out.println("main继续");

有人解释:

join() 把 t1 的优先级提高,所以 t1 一定抢占 CPU。

判断这个解释是否正确,并给出准确解释。

7.5 综合训练

编写“系统初始化模拟程序”。

系统需要完成:

线程 A:加载用户配置
线程 B:加载缓存数据
main:启动业务系统

要求:

  • A、B 分别具有明确线程名称。
  • A、B 使用 sleep() 模拟耗时。
  • A、B 同时启动。
  • main 必须等待 A、B 都执行结束。
  • 最终才输出:
系统启动完成

思考:

为什么这里需要 join,
而不是简单 sleep(5000)?

7.6 本章验收

不查看资料能够完成:

getName
setName
currentThread
sleep
join

五个 API 的口述与手写。

并且能够解释:

sleep 是等时间
join 是等线程
priority 不保证顺序
yield 不保证礼让
interrupt 不等于强杀
daemon 不阻止 JVM 进入关闭流程

即可进入下一章:

线程安全问题