JavaEE初阶(2):线程的概念、创建方式与 Thread 常用方法

🌸雨落在了我的手上:个人主页******

🐟个人仓库: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 为什么需要线程

并发编程已经成为刚需,主要有两方面原因:

  1. 单核 CPU 的性能提升遇到瓶颈。想提高算力,就需要多核 CPU,而并发编程可以充分利用多核 CPU 的资源。
  2. 有些任务需要"等待 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 观察线程:

  1. 保持 ThreadDemo 程序运行。
  2. 在命令行输入 jconsole 并回车,打开"Java 监视和管理控制台"。
  3. 双击选择 ThreadDemo 对应的进程,切换到"线程"标签页。
  4. 左侧的列表就是当前所有线程,可以看到 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() 已经被废弃)。正确的做法是通知线程,由线程自己判断并结束。

常见的通知方式有两种:

  1. 通过共享的标志位进行沟通
  2. 调用 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() 的输出,属于正常现象。

线程收到中断通知的两种情况:

  1. 线程正在阻塞 (调用了 sleep、wait、join 等方法):以 InterruptedException 异常的形式通知,并清除中断标志。
  2. 线程没有阻塞 (例如正在做计算):不会抛异常,只是设置中断标志,线程需要通过 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 后根据需要跳出循环

七、练习题

  1. 基础练习:用 Lambda 创建两个线程,一个循环打印 "A" 5 次,另一个循环打印 "B" 5 次,观察两者的输出是否交错。
  2. 理解练习 :在 5.5 的 interrupt() 示例中,把 catch 块里的 break; 删掉,运行后观察会发生什么,并思考原因。(提示:想一想中断标志在哪一步被清除了。)
  3. 应用练习 :修改 5.6 的 join() 示例,让王五先开始工作,李四后开始工作。

八、写在最后

这一部分主要解决了"怎么创建和控制线程 "的问题。接下来的内容会讨论线程的状态 ,以及多线程带来的核心难题------线程安全 ,还会介绍 synchronized 和 volatile 关键字。

如果你在学习过程中遇到疑问,欢迎在评论区留言交流!

相关推荐
我命由我123458 小时前
6-2.Python 异常处理 - 异常捕获(捕获指定异常、常见异常类型、异常信息)
java·运维·开发语言·python·http·https·java-ee
xieliyu.2 天前
【JavaEE进阶】Spring Boot 日志:从 System.out 到 @Slf4j,6 个级别 + 4 类配置一篇讲透
java·spring boot·spring·java-ee·springboot·logback·slf4j
Sirens.2 天前
网络通信基础:IP、端口、协议分层与数据封装
java·网络·网络协议·tcp/ip·java-ee·1024程序员节
木井巳3 天前
【JavaEE】SpringBoot 统一功能处理: 拦截器、统一返回与全局异常
java·spring boot·后端·spring·java-ee
我命由我123454 天前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
雨落在了我的手上4 天前
JavaEE初级(1):计算机是如何⼯作的
java·java-ee
我命由我123454 天前
Python 开发 - Python 项目部署(Windows 与 Linux)
java·linux·windows·后端·python·java-ee·python3.11
我命由我123455 天前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee
我命由我123456 天前
5-1.Python 编码进阶 - 函数进阶(参数、返回值、递归、函数作为参数传递、lambda 匿名函数)
java·服务器·数据库·后端·python·java-ee·python3.11