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

目录
一、认识线程
1.1 什么是线程
1.2 为什么需要线程
1.3 进程和线程的区别
1.4 Java 线程与操作系统线程的关系
二、第一个多线程程序
2.1 第一步:定义线程类
2.2 第二步:编写线程要执行的代码
2.3 第三步:编写 main 方法,启动线程
2.4 观察线程:使用 jconsole
三、创建线程的方式
3.1 方式一:继承 Thread 类
3.2 方式二:实现 Runnable 接口
3.3 其他写法:匿名内部类与 Lambda
四、多线程的优势:提高运行速度
4.1 先看常量
4.2 并发方式:concurrency()
4.3 串行方式:serial()
4.4 main 方法:安排执行顺序
4.5 运行结果与分析
五、Thread 类及常见方法
5.1 Thread 对象与线程的关系
5.2 常见构造方法
5.3 常见属性
5.4 启动线程:start() 与 run() 的区别
5.5 中断一个线程
方式一:使用自定义标志位
方式二:使用 interrupt() 方法
5.6 等待一个线程:join()
5.7 获取当前线程引用:Thread.currentThread()
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 为什么需要线程
并发编程已经成为刚需,主要有两方面原因:
另外,多进程也能实现并发,但线程比进程更轻量:创建线程比创建进程更快,销毁线程比销毁进程更快,调度线程也比调度进程更快。
1.3 进程和线程的区别
| 包含关系 | 进程包含线程,每个进程至少有一个主线程 | 线程属于某个进程 |
| 内存空间 | 进程之间不共享内存 | 同一进程的线程共享内存 |
| 资源与调度 | 进程是系统分配资源的最小单位 | 线程是系统调度的最小单位 |
| 故障影响 | 一个进程挂了一般不影响其他进程 | 一个线程挂了,可能带走同进程的其他线程(整个进程崩溃) |
可以这样理解内存共享:多个客户到银行各自办理业务,他们的票据彼此保密,这就像多进程,互不共享内存;而张三、李四、王五办的是同一家公司的业务,票据是共享的,这就是多线程。

1.4 Java 线程与操作系统线程的关系
线程是操作系统中的概念。操作系统内核实现了线程机制,并对用户层提供了一些 API(例如 Linux 的 pthread 库)。Java 标准库中的 Thread 类,可以看作是对操作系统线程 API 的进一步抽象和封装。也就是说,我们在 Java 里写的 Thread,最终都是由操作系统来真正执行的。
二、第一个多线程程序
在写代码之前,先明确这个程序要做什么:
- 创建一个新线程,让它不停地打印自己的名字
- 主线程(main)同时也不停地打印自己的名字
- 观察两者的输出是怎样"交错"出现的,感受并发
下面的代码都放在同一个文件 ThreadDemo.java 中,我们一段一段来看。
2.1 第一步:定义线程类
private static class MyThread extends Thread {
// 线程要做的事情写在这里
}
- private static class:这是定义在 ThreadDemo 内部的一个类。static 让它可以直接在 main 方法中使用。
- extends Thread:表示 MyThread 继承了 Thread 类。继承之后,它就拥有了线程的能力,可以调用 start() 启动。
2.2 第二步:编写线程要执行的代码
@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 方法,启动线程
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 循环。所以两个线程是"同时"在跑的。
运行后的输出大致如下(每次的顺序都可能不同):
main
Thread-0
Thread-0
main
main
Thread-0
…
💡 观察要点:只要看到 main 和 Thread-0 交错出现,就说明两个线程在并发执行。每次输出的顺序都不一样,是因为每次随机的休眠时间不同。
⚠️ 由于是 while (true),程序不会自动结束,需要点击 IDE 的停止按钮,或在命令行按 Ctrl + C 停止。
2.4 观察线程:使用 jconsole
程序运行的同时,我们还可以用 JDK 自带的工具 jconsole 观察线程:

三、创建线程的方式
创建线程有两种最基本的方式。无论用哪种,都遵循同一个原则:把要执行的代码写在 run() 方法里,然后调用 start() 启动线程。
3.1 方式一:继承 Thread 类
第一步,定义一个继承 Thread 的类,并重写 run() 方法:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- extends Thread:MyThread 成为了线程类。
- run():重写父类 Thread 中的 run() 方法,把线程要做的事情写在这里。
第二步,创建对象并启动:
MyThread t = new MyThread(); // 创建线程对象
t.start(); // 启动线程,run() 开始在新线程中执行
- new MyThread():创建线程对象,此时线程还没有运行。
- t.start():启动线程。系统会在新线程中自动调用 run(),我们不需要也不应该手动调用 run()。
⚠️ 完整的执行顺序是:new 创建对象 → start() 启动 → 系统自动在新线程中调用 run()。
3.2 方式二:实现 Runnable 接口
先认识 Runnable:它是 Java 提供的一个接口,只规定了一个方法 run()。可以把它理解为"一个任务"。注意:任务本身不是线程,需要交给 Thread 去执行。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- implements Runnable:表示 MyRunnable 实现了 Runnable 接口,必须实现其中的 run() 方法。
然后把任务交给线程:
MyRunnable task = new MyRunnable(); // 创建任务对象
Thread t = new Thread(task); // 把任务交给线程
t.start(); // 启动线程
- new Thread(task):创建线程,并把任务作为 target(目标任务)传入。线程启动后,会去执行任务的 run()。
可以这样类比:任务(MyRunnable)是"工作内容",线程(Thread)是"干活的人",两者组合起来才能把活干起来。
两种方式的对比:
| 关键字 | extends Thread | implements Runnable |
| this 指什么 | 当前线程对象 | 任务对象(MyRunnable) |
| 获取当前线程 | 可以直接用 this,也可以用 Thread.currentThread() | 只能用 Thread.currentThread() |
| 能否再继承其他类 | 不能(Java 只支持单继承) | 可以 |
📌 推荐使用实现 Runnable 的方式:任务和线程分开,类的继承关系更灵活。后面学习线程池时,你会更能体会到这种"任务与线程分离"的好处。
3.3 其他写法:匿名内部类与 Lambda
在实际开发中,经常会看到下面两种更简洁的写法。
(1)匿名内部类:当一个类只用一次时,可以不起名字,直接在 new 的时候写出它的内容:
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
t.start();
解析:这段代码的结构是 new Thread( new Runnable() { … } )。它相当于"没有名字地定义了一个 Runnable 类,立刻创建它的对象,再把这个对象传给 Thread"。
(2)Lambda 表达式:可以把上面的写法压缩成一行。先对比一下两者的效果是完全一样的:
// 写法 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 先看常量
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 步:记录开始时间
long begin = System.nanoTime();
- System.nanoTime():返回当前时间的纳秒数(1 秒 = 10 亿纳秒)。在计算前后各取一次时间,相减就是中间经过的时间。
第 2 步:创建并启动子线程,让它计算 a
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
int b = 0;
for (long i = 0; i < count; i++) {
b–;
}
- 这段代码在 main 线程中执行。此时子线程在计算 a,主线程在计算 b,两件事同时进行。
第 4 步:等待子线程结束
thread.join();
- join() 的作用是:让当前线程(这里是 main)停下来,等待 thread 执行完毕。
- 如果不写这一行,主线程可能很快算完 b,子线程还没结束,后面统计出来的耗时就不准确了。
第 5 步:统计耗时并输出
long end = System.nanoTime();
System.out.printf("并发: %f 毫秒%n", (end – begin) / 1e6);
- 1 毫秒 = 1,000,000 纳秒,所以把纳秒差值除以 1e6,就得到了毫秒数。
- printf 是格式化输出:%f 表示输出一个小数,%n 表示换行。
4.3 串行方式:serial()
串行方式全部在 main 线程中完成,一个循环做完再做下一个:
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 方法:安排执行顺序
public static void main(String[] args) throws InterruptedException {
concurrency(); // 先执行并发方式
serial(); // 再执行串行方式
}
- throws InterruptedException:因为 join() 可能抛出这个异常,所以在方法声明中注明,交由调用者处理。
4.5 运行结果与分析
并发: 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) | 同时指定任务和名字 |
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() |
先看一个简单的例子,感受这些属性的获取方式:
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() ≈ 喊一声"行动起来!",线程才真正独立去执行

用一段代码对比两者的区别:
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() 已经被废弃)。正确的做法是通知线程,由线程自己判断并结束。
常见的通知方式有两种:
方式一:使用自定义标志位
第 1 步:定义标志位
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false; // 标志位:false 表示继续工作,true 表示该停止了
// … run() 方法见下一步
}
- isQuit 就像一个"开关":默认 false,线程看到它变成 true 时就该结束了。
- volatile:保证一个线程修改了这个变量后,其他线程能及时看到新的值。关于它的原理,后面会详细讲,现在先记住:这里必须加上。
第 2 步:在 run() 中检查标志位
@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 中修改标志位
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() 中检查中断标志
@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 中发出中断通知
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() 的输出,属于正常现象。
线程收到中断通知的两种情况:
用一段代码理解 interrupted() 和 isInterrupted() 的区别:
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 步:定义工作内容
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 步:创建两个线程,并按顺序执行
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(),所以李四和王五是先后工作的,而不是同时工作。
运行效果(节选):
先让李四开始工作
李四: 我还在工作!
李四: 我还在工作!
…(共 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()
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()
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 后根据需要跳出循环
七、练习题
八、写在最后
这一部分主要解决了"怎么创建和控制线程"的问题。接下来的内容会讨论线程的状态,以及多线程带来的核心难题——线程安全,还会介绍 synchronized 和 volatile 关键字。
如果你在学习过程中遇到疑问,欢迎在评论区留言交流!



