
🌸雨落在了我的手上:个人主页******
🐟个人仓库:Gitee仓库******
❄️个人专栏:<<JaveSe*** >> <<C语言 >> <<C语言数据结构 >>*** <<Java数据结构*** >>***
<<MySQL数据库基础*** >> <<JavaEE初阶 >>***
🔥🔥🔥人生格言:无人扶我青云志,我自踏雪至山巅
🎬 博主简介

目录
[1.1 什么是线程](#1.1 什么是线程)
[1.2 为什么需要线程](#1.2 为什么需要线程)
[1.3 进程和线程的区别](#1.3 进程和线程的区别)
[1.4 Java 线程与操作系统线程的关系](#1.4 Java 线程与操作系统线程的关系)
[2.1 第一步:定义线程类](#2.1 第一步:定义线程类)
[2.2 第二步:编写线程要执行的代码](#2.2 第二步:编写线程要执行的代码)
[2.3 第三步:编写 main 方法,启动线程](#2.3 第三步:编写 main 方法,启动线程)
[2.4 观察线程:使用 jconsole](#2.4 观察线程:使用 jconsole)
[3.1 方式一:继承 Thread 类](#3.1 方式一:继承 Thread 类)
[3.2 方式二:实现 Runnable 接口](#3.2 方式二:实现 Runnable 接口)
[3.3 其他写法:匿名内部类与 Lambda](#3.3 其他写法:匿名内部类与 Lambda)
[4.1 先看常量](#4.1 先看常量)
[4.2 并发方式:concurrency()](#4.2 并发方式:concurrency())
[4.3 串行方式:serial()](#4.3 串行方式:serial())
[4.4 main 方法:安排执行顺序](#4.4 main 方法:安排执行顺序)
[4.5 运行结果与分析](#4.5 运行结果与分析)
[五、Thread 类及常见方法](#五、Thread 类及常见方法)
[5.1 Thread 对象与线程的关系](#5.1 Thread 对象与线程的关系)
[5.2 常见构造方法](#5.2 常见构造方法)
[5.3 常见属性](#5.3 常见属性)
[5.4 启动线程:start() 与 run() 的区别](#5.4 启动线程:start() 与 run() 的区别)
[5.5 中断一个线程](#5.5 中断一个线程)
[方式二:使用 interrupt() 方法](#方式二:使用 interrupt() 方法)
[5.6 等待一个线程:join()](#5.6 等待一个线程:join())
[5.7 获取当前线程引用:Thread.currentThread()](#5.7 获取当前线程引用:Thread.currentThread())
[5.8 休眠当前线程:sleep()](#5.8 休眠当前线程:sleep())
内容大纲:
本文系统讲解线程概念及Java多线程编程基础,涵盖线程与进程区别、创建线程的两种方式(继承Thread或实现Runnable)、Thread类常用方法(start()、join()、sleep()、interrupt()等),并通过实例演示线程并发执行、中断控制与等待机制。重点强调推荐使用"实现Runnable+Lambda"方式,提升代码灵活性,并通过计算实验直观展示多线程在多核环境下的性能优势。最后总结核心知识点与常见易错点,为后续学习线程安全奠定基础。
📘 本文学习目标
- 理解什么是线程,以及线程与进程的区别
- 学会两种创建线程的方式,并能看懂每一段代码的作用
- 认识 Thread 类的常用方法:
start()、join()、sleep()等- 掌握两种通知线程停止的方式:标志位 与
interrupt()📌 阅读前提 :会写简单的类、方法,会用
for和while循环,知道main方法是程序的入口即可。文中遇到新语法会顺带解释。
一、认识线程
1.1 什么是线程
**线程就是一个"执行流"。**每个线程都可以按照顺序执行自己的代码,多个线程之间可以"同时"执行多份代码。
用银行办业务来类比:一家公司要去银行办理财务转账、福利发放和缴纳社保。如果只有张三一个会计,忙不过来,耗时很长。于是张三找来同事李四、王五一起帮忙,三个人分别负责一件事,各自排队办理。这时就有了三个执行流共同完成任务,但本质上他们都是为了办理同一家公司的业务。
这种把一个大任务拆分成多个小任务、交给不同执行流分别执行的做法,就叫多线程 。其中张三是最先存在的执行流,称为主线程(Main Thread);李四和王五是张三叫来的,属于由主线程创建的新线程。
🔑 一句话记住:线程就是程序中的一个"执行流",多个线程可以同时各自执行自己的代码。
1.2 为什么需要线程
并发编程已经成为刚需,主要有两方面原因:
- 单核 CPU 的性能提升遇到瓶颈。想提高算力,就需要多核 CPU,而并发编程可以充分利用多核 CPU 的资源。
- 有些任务需要"等待 IO"。例如读写文件、网络请求时线程会空等,为了让等待 IO 的时间去做其他工作,也需要并发编程。
另外,多进程也能实现并发,但线程比进程更轻量:创建线程比创建进程更快,销毁线程比销毁进程更快,调度线程也比调度进程更快。
1.3 进程和线程的区别
| 对比项 | 进程 | 线程 |
|---|---|---|
| 包含关系 | 进程包含线程,每个进程至少有一个主线程 | 线程属于某个进程 |
| 内存空间 | 进程之间不共享内存 | 同一进程的线程共享内存 |
| 资源与调度 | 进程是系统分配资源的最小单位 | 线程是系统调度的最小单位 |
| 故障影响 | 一个进程挂了一般不影响其他进程 | 一个线程挂了,可能带走同进程的其他线程(整个进程崩溃) |
可以这样理解内存共享:多个客户到银行各自办理业务,他们的票据彼此保密,这就像多进程,互不共享内存;而张三、李四、王五办的是同一家公司的业务,票据是共享的,这就是多线程。

1.4 Java 线程与操作系统线程的关系
线程是操作系统中的概念。操作系统内核实现了线程机制,并对用户层提供了一些 API(例如 Linux 的 pthread 库)。Java 标准库中的 Thread 类,可以看作是对操作系统线程 API 的进一步抽象和封装。也就是说,我们在 Java 里写的 Thread,最终都是由操作系统来真正执行的。
二、第一个多线程程序
在写代码之前,先明确这个程序要做什么:
- 创建一个新线程,让它不停地打印自己的名字
- 主线程(main)同时也不停地打印自己的名字
- 观察两者的输出是怎样"交错"出现的,感受并发
下面的代码都放在同一个文件 ThreadDemo.java 中,我们一段一段来看。
2.1 第一步:定义线程类
java
private static class MyThread extends Thread {
// 线程要做的事情写在这里
}
private static class:这是定义在ThreadDemo内部的一个类。static让它可以直接在main方法中使用。extends Thread:表示MyThread继承了Thread类。继承之后,它就拥有了线程的能力,可以调用start()启动。
2.2 第二步:编写线程要执行的代码
java
@Override
public void run() {
Random random = new Random();
while (true) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
逐行解析:
run():线程要做的事情都写在这个方法里,可以把它理解为线程的"行动指南"。@Override:注解,表示"这个方法是在重写父类的方法"。如果方法名写错,编译器会报错提醒你。Random random = new Random();:创建一个随机数生成器,后面用来产生随机的休眠时间。while (true):死循环,线程会一直运行下去,直到程序被强制停止。System.out.println(...):在控制台打印一行内容,并自动换行。Thread.currentThread().getName():获取"当前正在执行这行代码的线程"的名字。Thread.currentThread()返回正在执行的那个线程对象,getName()返回它的名字。random.nextInt(10):返回 0 到 9 之间的随机整数。Thread.sleep(...):让当前线程暂停指定的毫秒数。这里的休眠时间是随机的,所以两个线程交替的节奏也是随机的。try { ... } catch (InterruptedException e):sleep方法要求必须处理InterruptedException(线程在睡眠时可能被别人打断)。现阶段我们只需要知道:出现这个异常时,用printStackTrace()把错误信息打印出来即可。
2.3 第三步:编写 main 方法,启动线程
java
public static void main(String[] args) {
MyThread t1 = new MyThread(); // ① 创建线程对象
t1.start(); // ② 启动线程
// ③ main 线程自己的工作
Random random = new Random();
while (true) {
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
逐行解析:
- ①
new MyThread():只是创建了线程对象,相当于"把李四叫过来,但还没让他行动"。此时新线程还没有开始运行。 - ②
t1.start():这一步才真正启动新线程,JVM 会让新线程去执行run()方法。注意start()调用后会立即返回,不会等run()执行完。 - ③ main 线程不会因为启动了
t1而停下来,它继续执行自己的while循环。所以两个线程是"同时"在跑的。
运行后的输出大致如下(每次的顺序都可能不同):
java
main
Thread-0
Thread-0
main
main
Thread-0
...
💡 观察要点 :只要看到
main和Thread-0交错出现,就说明两个线程在并发执行。每次输出的顺序都不一样,是因为每次随机的休眠时间不同。⚠️ 由于是
while (true),程序不会自动结束,需要点击 IDE 的停止按钮,或在命令行按Ctrl + C停止。
2.4 观察线程:使用 jconsole
程序运行的同时,我们还可以用 JDK 自带的工具 jconsole 观察线程:
- 保持
ThreadDemo程序运行。 - 在命令行输入
jconsole并回车,打开"Java 监视和管理控制台"。 - 双击选择
ThreadDemo对应的进程,切换到"线程"标签页。 - 左侧的列表就是当前所有线程,可以看到
main、Thread-0等名字。

三、创建线程的方式
创建线程有两种最基本的方式。无论用哪种,都遵循同一个原则:把要执行的代码写在 run() 方法里,然后调用 start() 启动线程。
3.1 方式一:继承 Thread 类
第一步,定义一个继承 Thread 的类,并重写 run() 方法:
java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
extends Thread:MyThread成为了线程类。run():重写父类Thread中的run()方法,把线程要做的事情写在这里。
第二步,创建对象并启动:
java
MyThread t = new MyThread(); // 创建线程对象
t.start(); // 启动线程,run() 开始在新线程中执行
new MyThread():创建线程对象,此时线程还没有运行。t.start():启动线程。系统会在新线程中自动调用run(),我们不需要也不应该手动调用run()。
⚠️ 完整的执行顺序是:new 创建对象 → start() 启动 → 系统自动在新线程中调用 run()。
3.2 方式二:实现 Runnable 接口
先认识 Runnable:它是 Java 提供的一个接口 ,只规定了一个方法 run()。可以把它理解为"一个任务"。注意:任务本身不是线程 ,需要交给 Thread 去执行。
java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
implements Runnable:表示MyRunnable实现了Runnable接口,必须实现其中的run()方法。
然后把任务交给线程:
java
MyRunnable task = new MyRunnable(); // 创建任务对象
Thread t = new Thread(task); // 把任务交给线程
t.start(); // 启动线程
new Thread(task):创建线程,并把任务作为target(目标任务)传入。线程启动后,会去执行任务的run()。
可以这样类比:任务(MyRunnable)是"工作内容",线程(Thread)是"干活的人",两者组合起来才能把活干起来。
两种方式的对比:
| 对比项 | 继承 Thread | 实现 Runnable |
|---|---|---|
| 关键字 | extends Thread |
implements Runnable |
this 指什么 |
当前线程对象 | 任务对象(MyRunnable) |
| 获取当前线程 | 可以直接用 this,也可以用 Thread.currentThread() |
只能用 Thread.currentThread() |
| 能否再继承其他类 | 不能(Java 只支持单继承) | 可以 |
📌 推荐使用实现 Runnable 的方式:任务和线程分开,类的继承关系更灵活。后面学习线程池时,你会更能体会到这种"任务与线程分离"的好处。
3.3 其他写法:匿名内部类与 Lambda
在实际开发中,经常会看到下面两种更简洁的写法。
(1)匿名内部类 :当一个类只用一次时,可以不起名字,直接在 new 的时候写出它的内容:
java
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
t.start();
解析:这段代码的结构是 new Thread( new Runnable() { ... } )。它相当于"没有名字地定义了一个 Runnable 类,立刻创建它的对象,再把这个对象传给 Thread"。
(2)Lambda 表达式:可以把上面的写法压缩成一行。先对比一下两者的效果是完全一样的:
java
// 写法 A:匿名内部类
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
});
// 写法 B:Lambda 表达式(效果与写法 A 完全相同)
Thread t2 = new Thread(() -> {
System.out.println("Hello");
});
Lambda 的语法拆解:
():对应run()方法的参数列表。run()没有参数,所以是空括号。->:读作"箭头",把参数和方法体分隔开。{ ... }:run()方法的方法体,也就是线程要做的事情。- 如果方法体只有一句话,大括号也可以省略:
new Thread(() -> System.out.println("Hello"));
📌 学习建议:先熟练掌握"实现 Runnable + Lambda"的写法,这是实际开发中最常见的形式。匿名内部类了解其含义即可。
四、多线程的优势:提高运行速度
这一节通过一个计算实验,让你亲眼看到多线程在什么情况下能提速。
实验内容:分别用"并发"和"串行"两种方式,完成同样的计算------对变量 a 做 10 亿次 a--,对变量 b 做 10 亿次 b--,比较两者耗时。
4.1 先看常量
java
private static final long count = 10_0000_0000L;
private static final:private表示外部不能直接访问;static表示它属于类本身;final表示它是常量,值不能被修改。long:64 位整数类型,可以表示很大的数字。10_0000_0000L:就是 1,000,000,000(十亿)。Java 允许在数字中间加下划线,只是为了方便阅读;末尾的L表示这是long类型的数字。
4.2 并发方式:concurrency()
这个方法分为 5 个步骤,我们逐步看。
第 1 步:记录开始时间
java
long begin = System.nanoTime();
System.nanoTime():返回当前时间的纳秒数(1 秒 = 10 亿纳秒)。在计算前后各取一次时间,相减就是中间经过的时间。
第 2 步:创建并启动子线程,让它计算 a
java
Thread thread = new Thread(() -> {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
});
thread.start();
- 这里用 Lambda 写了一个任务:循环
count次,每次执行a--。 thread.start()启动后,这段循环就在子线程中执行。- 变量
a定义在 Lambda 内部,只属于这个子线程。这里的a没有被使用,只是为了模拟一段"耗时的计算"。
第 3 步:主线程同时计算 b
java
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
- 这段代码在
main线程中执行。此时子线程在计算a,主线程在计算b,两件事同时进行。
第 4 步:等待子线程结束
java
thread.join();
join()的作用是:让当前线程(这里是main)停下来,等待thread执行完毕。- 如果不写这一行,主线程可能很快算完
b,子线程还没结束,后面统计出来的耗时就不准确了。
第 5 步:统计耗时并输出
java
long end = System.nanoTime();
System.out.printf("并发: %f 毫秒%n", (end - begin) / 1e6);
- 1 毫秒 = 1,000,000 纳秒,所以把纳秒差值除以
1e6,就得到了毫秒数。 printf是格式化输出:%f表示输出一个小数,%n表示换行。
4.3 串行方式:serial()
串行方式全部在 main 线程中完成,一个循环做完再做下一个:
java
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) { // 先算 a,算完才继续
a--;
}
int b = 0;
for (long i = 0; i < count; i++) { // 再算 b
b--;
}
long end = System.nanoTime();
System.out.printf("串行: %f 毫秒%n", (end - begin) / 1e6);
- 只有一个执行流,两个循环依次执行,总耗时 ≈ 两个循环的耗时之和。
- 对比并发方式:并发方式中两个循环是同时进行的。
4.4 main 方法:安排执行顺序
java
public static void main(String[] args) throws InterruptedException {
concurrency(); // 先执行并发方式
serial(); // 再执行串行方式
}
throws InterruptedException:因为join()可能抛出这个异常,所以在方法声明中注明,交由调用者处理。
4.5 运行结果与分析
java
并发: 399.651856 毫秒
串行: 720.616911 毫秒
- 并发耗时约为串行的一半多,原因是两个循环可以同时运行在两个 CPU 核心上。
- 结果会因电脑配置不同而有差异。如果你的电脑只有单核 CPU,并发可能就没有明显优势。
🤔 动手试一试:
- 把
count改成 1000,再运行一次。你会发现串行可能反而更快------因为创建线程、切换线程本身也需要开销,任务太小时,这些开销占了主要部分。- 把
thread.join();注释掉,再运行一次,观察耗时数字的变化,思考原因。
五、Thread 类及常见方法
5.1 Thread 对象与线程的关系
Thread 类是 JVM 用来管理线程的类。每个线程都有一个唯一的 Thread 对象与之关联。
可以把线程想象成办事的人,Thread 对象就是这个人的"档案",上面记录了 ID、名字、优先级、状态等信息。JVM 会把这些"档案"组织起来,用于线程的调度和管理。

5.2 常见构造方法
| 构造方法 | 说明 |
|---|---|
Thread() |
创建线程对象,没有指定任务,名字自动生成 |
Thread(Runnable target) |
指定任务(target),名字自动生成 |
Thread(String name) |
指定名字,没有任务 |
Thread(Runnable target, String name) |
同时指定任务和名字 |
java
Thread t1 = new Thread(); // 没有任务,名字自动生成
Thread t2 = new Thread(new MyRunnable()); // 有任务,名字自动生成
Thread t3 = new Thread("这是我的名字"); // 只有名字,没有任务
Thread t4 = new Thread(new MyRunnable(), "这是我的名字"); // 任务和名字都有
- target(任务) :线程启动后要执行的
Runnable对象。 - name(名字):主要用于调试。在 jconsole 里看到的线程名就是这里设置的。
- 如果没有指定名字,系统会自动分配
Thread-0、Thread-1等名字。 t1没有任务,启动后不会执行任何代码。
5.3 常见属性
| 属性 | 获取方法 |
|---|---|
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
先看一个简单的例子,感受这些属性的获取方式:
java
Thread t = new Thread(() -> System.out.println("我在工作"), "李四");
System.out.println(t.getName()); // 输出:李四
System.out.println(t.getState()); // 输出:NEW(刚创建,还没 start)
System.out.println(t.isAlive()); // 输出:false(还没 start,所以不存活)
t.start();
各属性的含义:
- ID:线程的唯一编号,由 JVM 分配,不会重复。
- 名称:调试时用来区分不同线程。
- 状态 :线程当前所处的阶段(如
NEW、RUNNABLE),第三节会进一步讲解。 - 优先级:数字越大优先级越高。优先级高的线程理论上更容易被调度到,但并不保证。
- 是否存活 :简单理解为
run()是否还没有执行完。start()之前和run()结束之后都是 false。 - 是否后台线程 :后台线程(守护线程)通常用来做辅助工作。需要记住一点:JVM 会在所有非后台线程结束后才退出 。也就是说,只要
ThreadDemo中那个while (true)的线程没结束,程序就不会退出。 - 是否被中断:线程是否收到了中断通知,5.5 节会详细讲。
5.4 启动线程:start() 与 run() 的区别
覆写 run() 方法,只是告诉线程"要做什么事情"的指令清单。此时线程对象只是被创建出来,并不会真正运行 ,必须调用 start() 才行。
可以这样类比:
- 线程对象 ≈ 把李四、王五叫过来
run()≈ 给他们一份行动指南start()≈ 喊一声"行动起来!",线程才真正独立去执行

用一段代码对比两者的区别:
java
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // 输出:main (普通方法调用,在当前的 main 线程中执行)
t.start(); // 输出:Thread-0 (启动新线程,在新线程中执行)
t.run()只是一个普通的方法调用,代码在main线程中执行,没有创建新线程。t.start()才会在操作系统中真正创建一个新线程,并在新线程中执行run()。
⚠️ 易错点 :同一个线程对象只能
start()一次。第二次调用会抛出IllegalThreadStateException异常。如果需要再次执行,应该重新new一个线程对象。
5.5 中断一个线程
线程一旦开始工作,就会按照 run() 中的指令一直执行,直到执行完毕。但有时我们需要从外部通知它停下来。
比如:张三正在通过李四转账,老板突然来电话,说转账对象是骗子,需要赶紧停止转账。这就需要"停止线程"的机制。
📌 注意 :Java 不提供"强制杀死线程"的安全方法(
Thread.stop()已经被废弃)。正确的做法是通知线程,由线程自己判断并结束。
常见的通知方式有两种:
- 通过共享的标志位进行沟通
- 调用
interrupt()方法进行通知
方式一:使用自定义标志位
第 1 步:定义标志位
java
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false; // 标志位:false 表示继续工作,true 表示该停止了
// ... run() 方法见下一步
}
isQuit就像一个"开关":默认false,线程看到它变成true时就该结束了。volatile:保证一个线程修改了这个变量后,其他线程能及时看到新的值。关于它的原理,后面会详细讲,现在先记住:这里必须加上。
第 2 步:在 run() 中检查标志位
java
@Override
public void run() {
while (!isQuit) { // 每次循环前先检查开关
System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000); // 每隔 1 秒转一次账(模拟工作)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 啊!险些误了大事");
}
while (!isQuit):当isQuit为false时,!isQuit为true,循环继续。- 当
isQuit被改成true后,!isQuit变为false,循环结束,执行最后那句收尾的打印。
第 3 步:在 main 中修改标志位
java
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable(); // 保存任务对象,后面要修改它的 isQuit
Thread thread = new Thread(target, "李四");
System.out.println("main: 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000); // 主线程等待 10 秒
System.out.println("main: 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true; // 修改开关,通知李四停止
}
MyRunnable target = new MyRunnable();:必须用变量保存这个对象,才能在后面通过它修改isQuit。Thread.sleep(10 * 1000):10 * 1000等于 10000 毫秒,也就是 10 秒。target.isQuit = true:修改开关。李四的while循环在下一次检查时就会退出。
运行效果:李四每隔约 1 秒输出一次"别管我",大约持续 10 秒;之后 main 输出"老板来电话了",李四在下一次检查时退出循环,输出"啊!险些误了大事"。
方式二:使用 interrupt() 方法
每个 Thread 对象内部都有一个 boolean 类型的中断标志 ,默认为 false。相关的三个方法如下:
| 方法 | 说明 |
|---|---|
public void interrupt() |
中断对象关联的线程:设置它的中断标志;如果线程正在阻塞(如 sleep),还会抛出 InterruptedException |
public static boolean interrupted() |
判断当前 线程的中断标志是否被设置,调用后会清除标志 |
public boolean isInterrupted() |
判断对象关联 线程的中断标志是否被设置,调用后不清除标志 |
第 1 步:在 run() 中检查中断标志
java
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) { // 检查自己是否被通知中断
System.out.println(Thread.currentThread().getName() + ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) { // 睡眠时被中断,会进入这里
e.printStackTrace();
System.out.println(Thread.currentThread().getName() + ": 有内鬼,终止交易!");
break; // 手动跳出循环
}
}
System.out.println(Thread.currentThread().getName() + ": 啊!险些误了大事");
}
Thread.currentThread().isInterrupted():检查"当前线程"的中断标志。它只是查看,不会清除标志。catch块中的break非常重要。原因是:线程在sleep中被中断时,JVM 会抛出异常,同时清除中断标志 。如果不写break,while的条件检查会认为"没有被中断",循环就会继续下去,停不下来。
第 2 步:在 main 中发出中断通知
java
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "李四");
System.out.println("main: 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println("main: 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt(); // 通知李四:该停了
}
运行效果 :和方式一类似,李四输出约 10 次"别管我",然后在 sleep 时被中断,输出"有内鬼,终止交易!",最后输出"啊!险些误了大事"。控制台中间还会打印一段异常的堆栈信息,这是 printStackTrace() 的输出,属于正常现象。
线程收到中断通知的两种情况:
- 线程正在阻塞 (调用了
sleep、wait、join等方法):以InterruptedException异常的形式通知,并清除中断标志。 - 线程没有阻塞 (例如正在做计算):不会抛异常,只是设置中断标志,线程需要通过
isInterrupted()主动检查。
用一段代码理解 interrupted() 和 isInterrupted() 的区别:
java
Thread.currentThread().interrupt(); // 给当前线程(main)设置中断标志
System.out.println(Thread.currentThread().isInterrupted()); // true,只查看,不清除
System.out.println(Thread.interrupted()); // true,查看并清除
System.out.println(Thread.interrupted()); // false,标志已经被清除了
💡 记忆方法 :带
static的interrupted()"会清除",对象方法isInterrupted()"不会清除"。
5.6 等待一个线程:join()
有时我们需要等一个线程完成工作之后,才能进行自己的下一步。例如:张三只有等李四转账成功,才决定是否存钱。这时就要用 join()。
第 1 步:定义工作内容
java
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
- 这是一个 Lambda 写的任务:循环 10 次,每次打印一句话并休眠 1 秒,所以整个任务大约需要 10 秒。
- 这个任务会被李四和王五共用,两人做的是同样的工作。
第 2 步:创建两个线程,并按顺序执行
java
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join(); // main 在这里停住,直到李四结束
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join(); // main 在这里停住,直到王五结束
System.out.println("王五工作结束了");
thread1.join():调用join()的线程(这里是main)会阻塞等待 ,直到thread1执行结束,才会继续往下走。- 因为有两次
join(),所以李四和王五是先后工作的,而不是同时工作。
运行效果(节选):
java
先让李四开始工作
李四: 我还在工作!
李四: 我还在工作!
...(共 10 次)
李四: 我结束了!
李四工作结束了,让王五开始工作
王五: 我还在工作!
...(共 10 次)
王五: 我结束了!
王五工作结束了
🤔 思考题 :把两个
join()注释掉再运行,输出顺序会发生什么变化?提示:此时main不会等待,几个线程会同时工作,输出的内容会交错出现。动手试一试,看看是不是这样。
join() 的几种重载形式:
| 方法 | 说明 |
|---|---|
public void join() |
一直等待,直到线程结束 |
public void join(long millis) |
最多等待 millis 毫秒,超时后不再等待 |
public void join(long millis, int nanos) |
同上,但精度可以到纳秒 |
5.7 获取当前线程引用:Thread.currentThread()
java
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread(); // 获取正在执行 main 方法的那个线程
System.out.println(thread.getName()); // 输出:main
}
}
currentThread()是静态方法,直接通过类名调用即可。- 它返回的是正在执行这行代码的线程 对象。在
Runnable的run()中,this指的是任务对象,而不是线程,所以需要用Thread.currentThread()来获取线程。
5.8 休眠当前线程:sleep()
java
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis()); // 第一次取时间(毫秒)
Thread.sleep(3 * 1000); // 休眠 3 秒
System.out.println(System.currentTimeMillis()); // 第二次取时间(毫秒)
}
}
System.currentTimeMillis():返回从 1970 年 1 月 1 日 0 点(UTC)到现在的毫秒数。Thread.sleep(3 * 1000):让当前线程(这里是 main)休眠 3000 毫秒,也就是 3 秒。- 两次输出的差值通常会略大于 3000。
⚠️ 注意 :线程的调度不是程序员能控制的,所以
sleep只能保证实际休眠时间大于等于设定的时间,而不是精确等于。
| 方法 | 说明 |
|---|---|
public static void sleep(long millis) |
休眠当前线程 millis 毫秒 |
public static void sleep(long millis, int nanos) |
可以更高精度的休眠 |
六、本节知识小结
| 知识点 | 核心要点 |
|---|---|
| 线程 | 一个"执行流",多线程即多个执行流并发执行 |
| 线程与进程 | 进程包含线程;同一进程的线程共享内存;线程更轻量 |
| 创建线程 | 继承 Thread 或实现 Runnable,推荐后者;配合 Lambda 使用更简洁 |
| 启动线程 | 必须调用 start(),直接调用 run() 不会开启新线程 |
| 中断线程 | 使用标志位或 interrupt();阻塞状态下会抛出 InterruptedException,并清除中断标志 |
| 等待线程 | join() 让当前线程等待目标线程结束 |
| 休眠线程 | sleep() 保证休眠时长至少为设定值 |
易错点速查
- ❌ 直接调用
run()以为开启了新线程 → ✅ 应调用start() - ❌ 同一个线程对象调用两次
start()→ ✅ 每个线程对象只能启动一次 - ❌ 统计耗时时忘记
join(),结果不准确 → ✅ 等子线程结束后再统计 - ❌ 中断处理中忘记写
break,线程停不下来 → ✅ 捕获InterruptedException后根据需要跳出循环
七、练习题
- 基础练习:用 Lambda 创建两个线程,一个循环打印 "A" 5 次,另一个循环打印 "B" 5 次,观察两者的输出是否交错。
- 理解练习 :在 5.5 的
interrupt()示例中,把catch块里的break;删掉,运行后观察会发生什么,并思考原因。(提示:想一想中断标志在哪一步被清除了。) - 应用练习 :修改 5.6 的
join()示例,让王五先开始工作,李四后开始工作。
八、写在最后
这一部分主要解决了"怎么创建和控制线程 "的问题。接下来的内容会讨论线程的状态 ,以及多线程带来的核心难题------线程安全 ,还会介绍 synchronized 和 volatile 关键字。
如果你在学习过程中遇到疑问,欢迎在评论区留言交流!