【多线程】Thread 类

目录

一、认识线程

[二、Java 中的线程 和 操作系统线程 的关系](#二、Java 中的线程 和 操作系统线程 的关系)

三、创建线程

[3.1 继承 Thread,重写 run](#3.1 继承 Thread,重写 run)

[3.2 实现 Runnable,重写 run](#3.2 实现 Runnable,重写 run)

[3.3 基于3.1,使用匿名内部类](#3.3 基于3.1,使用匿名内部类)

[3.4 基于3.2,使用匿名内部类](#3.4 基于3.2,使用匿名内部类)

[3.5 【推荐】使用 lambda 表达式](#3.5 【推荐】使用 lambda 表达式)

📎回调函数

[3.6 使用 jconsole 命令观察线程](#3.6 使用 jconsole 命令观察线程)

[3.7 Thread 常见构造方法](#3.7 Thread 常见构造方法)

[四、Thread 的常见属性](#四、Thread 的常见属性)

[4.1 获取当前线程的引用](#4.1 获取当前线程的引用)

[4.2 后台线程](#4.2 后台线程)

[4.3 是否存活](#4.3 是否存活)

[4.4 是否中断](#4.4 是否中断)

📎变量捕获

[4.5 等待一个线程](#4.5 等待一个线程)

[4.6 休眠线程](#4.6 休眠线程)

五、线程状态

六、总结


一、认识线程

线程的产生:由于单核 CPU 的发展遇到瓶颈,为了提高算力,就出现了多核 CPU。并发编程能够更充分地利用多核 CPU。而并发编程就是需要多个线程"同时"执行多份代码。

虽然多进程也能实现并发编程,但是线程比进程更轻量。(--->线程与进程的区别

二、Java 中的线程 和 操作系统线程 的关系

线程是 CPU 调度和执行的最小单位,是操作系统中的概念。操作系统内核实现了线程这种机制,并提供了一些 API 供用户使用。

但是操作系统提供的原生线程 API 是由 C 编写的,因此 Java 对操作系统提供的 API 进行了封装,并导入到标准库 (java.lang.)中,也就是 Thread 类。

因为 java.lang. 中所有类是默认导入到 Java 中的,因此使用 Thread 类时,不需要 import。

三、创建线程

创建线程有以下5种方法:

  1. 继承 Thread 类,重写 run 方法;
  2. 实现 Runnable 接口,重写 run();
  3. 继承 Thread,重写 run 方法,使用匿名内部类;
  4. 实现 Runnable,重写 run(),使用匿名内部类;
  5. 使用 lambda 表达式。

PS:当我们点进 Thread 原代码时,发现 Thread 实现了 Runnable 接口,而 Runnable 接口中只有一个 run 方法:

3.1 继承 Thread,重写 run

java 复制代码
class myThread extends Thread{
    @Override
    public void run() {
        // 使用while进行循环,方便查看效果
        while (true){
            System.out.println("Hello thread");
            // 使用 sleep() 必须抛异常
            // sleep 是静态方法,作用是休眠,让当前线程暂时放弃CPU资源
            try {
                // 单位是毫秒,1000ms就是1秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 此处无法使用 throws,只能用 try-catch
                // 因为父类 Thread run 中没有 throws
                // 默认是向上抛出异常,实际开发中推荐使用日记框架记录异常信息
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        // 创建实例,属于向上转型
        Thread t = new myThread();
        // 调用方法
        // t.run();
        t.start();

        while (true){
            System.out.println("Hello main.");
            Thread.sleep(1000);
        }
    }
}

注意:使用 t.run(),并没有真正创建线程,只是直接调用了重写的 run():

run() 相当于 Thread 的入口,新的线程启动就会自动执行 run() 方法,因此 run 不需要手动调用。

调用 start() 才是真正创建了线程,且每个 Thread 对象都只能 start() 一次

start() 是 JVM 提供的方法,本质上是调用操作系统的 API。

观察输出结果,可见有时候是 main 在前,有时候却是 thread 在前。这是因为多个线程执行时,它们的调度顺序是随机的,谁先执行是无法预测的。

3.2 实现 Runnable,重写 run

此时的 Runnable runnable 相当于一个"可以执行的任务",只是一段逻辑,最终还是要通过 Thread 真正创建出线程。

线程要做的工作在 Runnable 中表示,而不是直接重写 Thread 中的 run,能够解耦合****(任务和 Thread 线程这个概念解耦),后续如果要修改代码,采用这种方法会更简便。

java 复制代码
class myThread2 implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new myThread2();
        Thread t = new Thread(runnable);
        t.start();
        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

3.3 基于3.1,使用匿名内部类

上面两个方法中,我们新建的类都被赋予了名字:myThread 和 myThread2,而所谓的"匿名内部类"就是没有名字,直接在 {} 里面编写子类定义的代码,从而创建了这个匿名内部类的实例,并把实例的引用赋值给 t。

回顾------使用匿名内部类的前提:

  1. 必须有父类或接口;
  2. 只能继承一个类或实现一个接口;
  3. 必须实现所有的抽象方法;
  4. 仅能使用一次,无法再次创建该匿名类的实例。

3.4 基于3.2,使用匿名内部类

java 复制代码
public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Runnable r = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        Thread t = new Thread(r);
        t.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

3.5 【推荐】使用 lambda 表达式

Lambda 本质上是一个匿名函数,只能用于实现函数式接口(即只有一个抽象方法的接口)。

最主要的用途是作为 "回调函数" 被调用。

java 复制代码
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        while (true){
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

📎回调函数

回调函数 本质上是一种 将代码作为参数传递给其他代码的编程模式。

简单说就是:你定义了一段逻辑,但不立即执行,而是把它"交给"某个方法或框架,由对方在合适的时机去调用它。

① 上面代码中的 Lambda 表达式就是一个回调函数:

定义行为:Lambda 中定义了"每隔一秒打印 hello thread "这个任务

传递行为:将这个任务作为参数传递给 Thread 的构造函数

触发执行:当调用 t.start() 时,线程会在合适的时机(启动后)自动执行这个回调

② 为什么叫"回调"?

因为代码的 执行控制权 发生了反转

我们编写了 Lambda 中的逻辑,但没有直接调用它,而是把它交给了 Thread 对象,由Thread 在合适的时机回调这段代码。

③ 回调的执行流程:


3.6 使用 jconsole 命令观察线程

jconsole 在 JDK 安装路径下的 bin 包里面。

* 可以在 IDEA 的 File -> Project Structure 查看 JDK 的安装路径,从而找到 jconsole

如下,点击本地进程,选择 Demo1,连接之后选择"不安全的连接",再点击任务栏中的"连接"

线程被默认命名为 Thread-0,多个线程则由此递增:Thread-1,Thread-2...

除了 main 和 Thread-0 线程外,其他的都是 JVM 内置的线程,启动任何一个 Java 进程,都会自带这些线程,而它们的专有名词叫做 "后台线程"(下文提及)。

点击任何一条线程右边显示的信息中 "堆栈跟踪"下面的信息是指线程的调用栈 ,获取 线程状态(下文提及)的时刻,显示线程中的代码执行到哪里了。

3.7 Thread 常见构造方法

|--------------------------------------|--------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 对象传教线程对象,并命名 |

java 复制代码
// 方法1
Thread t1 = new Thread();

// 方法2
Runnable runnable = new myThread();
Thread t2 = new Thread(runnable);
// Thread t2 = new Thread(new myThread());

给线程命名是为了方便程序员进行调试,命名时一般起具有代表性、有意义的名称:

java 复制代码
// 方法3
Thread t3 = new Thread("线程1");

// 用 lambda 表达式时命名方法
Thread t3 = new Thread(() -> {
    // ...
},"线程1");


// 方法4
Thread t4 = new Thread(new myThread(), "线程2");

示例:

java 复制代码
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true){
                System.out.println("hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            while (true){
                System.out.println("hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t2");
        t2.start();

        Thread t3 = new Thread(() -> {
            while (true){
                System.out.println("hello t3");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        },"t3");
        t3.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
    }
}

当我们运行上述代码,发现主线程 main 输出3次结果之后程序还在运行。而查看 jconsole 时,发现 mian 线程已销毁。按照以前单核的思路,main 方法结束之后,整个程序就结束了。但现在多核 CPU 下,主线程结束并不代表整个进程结束。

四、Thread 的常见属性

|------------|-----------------|
| 属性 | 获取方法 |
| ID,线程的唯一标识 | getId() |
| 名称 | getName() |
| 优先级 | getPriority |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
| 状态 | getState() |

4.1 获取当前线程的引用

因为 Lambda 的定义是在 new Thread 之前,即在 Thread t 声明之前,所以无法获取引用变量 t。正确的获取方法是使用 Thread.currentThread() 这个静态方法。哪个线程中调用,获取到的就是哪个线程的 Thread 引用。

4.2 后台线程

JVM 会在一个进程的所有非后台线程结束后,才会结束运行。

非后台线程也叫前台线程,像上文3.6中涉及到的 main 和 Thread-0 就是前台线程,只有它们两个结束,整个进程才会结束。也就是说,后台线程的结束并不会影响进程的结束,但进程结束它们也就随之结束。

判断是否是后台线程,使用isDaemon() 方法;设置成后台线程,使用 **setDaemon()**方法。

示例:

java 复制代码
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
           while (true){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
        });
        // 设置后台线程得在 start 之前
        t.setDaemon(true);
        t.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("hello main");
            Thread.sleep(1000);
        }
        System.out.println(t.isDaemon());
        System.out.println("main 结束了");
    }
}

4.3 是否存活

可以简单地理解为 run 方法是否结束。

复杂的理解:

当你在 Java 中创建一个 Thread 对象并调用 start() 时:

JVM 会向操作系统请求创建一个真实的系统线程;

Java 的 Thread 对象与该系统线程建立了1对1的绑定关系

java 复制代码
Thread t = new Thread(() -> {...});  // 1. 创建Java对象
t.start();                            // 2. JVM 创建系统线程,建立映射

Thread 对象和系统线程两者的生命周期不同:

当执行完 run() 方法之后,系统线程就被销毁,而 Thread 对象还"存活"。

java 复制代码
Thread t = new Thread(() -> {
    System.out.println("线程运行中");
    // 任务执行完毕,线程自然结束
});

t.start();  // 系统线程创建并运行
// 当 run() 方法执行完毕,系统线程被销毁
// 但是!Thread 对象 t 仍然存在
System.out.println(t.isAlive());  // false - 系统线程已死
// t 这个对象还在内存中,只是它绑定的系统线程已经不在了

示例:

java 复制代码
public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        
        // 在调用 start 之前,没有真正创建线程,所以这里的结果一定是 false
        System.out.println(t.isAlive());

        t.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(t.isAlive());
            Thread.sleep(1000);
        }

        // t 对象仍然可以被访问
        System.out.println("Thread 对象还在:"+t.getName());
    }
}

· 根据 随机调度 的规则,打印的结果可能是 4 个 true,也可能是 3 个 true。

4.4 是否中断

"中断"的表达并不准确,而应该是判断是否"终止"。

如果未知 Java 提供的判断是否中断的方法,使用自定义变量来判断线程是否中断的代码如下:

java 复制代码
public class Demo4 {
    private static boolean isFinished = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!isFinished){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread 结束");
        });
        t.start();

        Thread.sleep(3000);
        isFinished = true;
    }
}

📎变量捕获

如果要将 isFinished 定义成局部变量,是否可行?

不可行,给出了警告:Lambda 表达式中的变量必须是 final 或 等同于 final。

为什么有这个限制?

局部变量是存储在栈中的,当main方法执行完毕(执行到第27行,但子线程thread还没结束),栈帧被销毁,局部变量就不存在了。但因为子线程可能还在运行,Lambda 表达式如果直接访问这个变量,就会出现"访问已销毁变量"的问题。

Java 通过 变量捕获 解决上面的问题:Lambda 会复制外部变量的值,而不是直接引用。

复制,意味着这样的变量不适合修改,因为即使修改了一方,另一方并不会随之变化(也就是说本质上是两个变量)。因此 Java 不允许修改:要么定义成 final,要么就不要做任何修改(也就是等同于 final)。

如果 Lambda 捕获的变量是引用类型呢?

Lambda 捕获局部变量时,遵循 值捕获 原则:

基本类型:捕获的是值本身,不能修改;

引用类型:捕获的是引用(地址),不能修改引用指向,但可以通过引用修改对象的内容。


但 isFinished 是成员变量时,就不再是"变量捕获"的语法,而切换到 "内部类访问外部类成员" 的语法。Lambda 表达式本质上是函数式接口,相当于一个内部类,内部类本来就能够访问外部类的成员。

而成员变量生命周期是由GC(垃圾回收)来管理的。在 Lambda 里面不担心变量生命周期失效的问题,也就不需要复制变量,从而不必限制用 final 修饰之类的问题。


前面都是铺垫内容,下面将讲解 Java 的 Thread 对象提供的用于判定线程是否中断的方法------isInterrupted(),以及设置中断的方法------ interrupt()

java 复制代码
public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    break; // 关键
                }
            }
            System.out.println("thread 结束");
        });
        t.start();

        Thread.sleep(3000);
        System.out.println("main 线程尝试阻止 t 线程......");
        t.interrupt(); // 将t终止
    }
}

上面的代码逻辑是一旦 t 线程被中断,捕捉到异常将会跳出循环结束线程。那么如果 catch 语句里面什么都不写呢?

我们发现 t 线程仍在运行。

正常情况下,调用 interrupt 方法就会修改 isInterrupted 方法内部的标志位,将其设置为 true;但是上述代码中的interrupt 将 sleep 唤醒了。这种被提前唤醒的情况下,sleep 就会在唤醒之后,将 isInterrupted 的标志位设置回 false。因此线程如果继续执行循环的条件判定,就会发现能够继续执行。

为此 catch 语句块应该如何编写,可以由程序员自行决定,可以让线程立即结束,还是等会结束,还是继续执行。

4.5 等待一个线程

多个线程之间并发执行时,根据随机调度原则,哪个线程先结束哪个后结束,程序员并不清楚。而 join() 方法能够设置一个线程等待其他线程结束之后,再进行它的工作。

使用 join 时,也需要抛出异常。

虽然也可以通过 sleep 设置休眠时间,来控制线程结束的先后顺序,但是有的情况下,这样的设定并不科学。

例如,希望 t 先结束,main 就紧跟着结束,如果通过设置休眠时间的方式,则并不靠谱:

java 复制代码
public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();

        t.join();
        System.out.println("main 结束了");
    }
}

上述代码执行到 t.join(); 时,main 线程就会"阻塞等待",一直等到 t 线程执行完毕,join 才继续执行。

我们会留意到上面 t 线程是被限制执行次数的,如果 t 进入了循环,主线程就会一直等下去,这种做法并不好。更科学的做法是设置好等待时间,一旦超过这个时间,join 就不等了:

java 复制代码
t.join(3000);
t.join(3000,500);

单个参数单位是毫秒,设置两个参数则第一个参数单位是毫秒,第二个参数单位是纳秒。

一般不会设置到 纳秒 这个精确位,因为计算机中(尤其是应用程序中)很难进行 ns 级别的精准时间测量。针对精准的时间计算,有一类操作系统,即"实时操作系统"可以做到,比如应用于工业/航天/军事等领域的实时操作系统。

4.6 休眠线程

因为线程的调度是不可控的,所以 sleep 休眠方法只能保证 实际休眠时间是 ≥ 参数设置的休眠时间的。

代码调用 sleep,相当于要当前线程让出 CPU 资源,后续设置的时间到了的时候,需要操作系统内核,就把这个线程重新调到 CPU 上,才能继续执行。但设置时间到了,并不意味着立即被执行,而只是意味着允许被调度了。

sleep 方法的特殊写法:t.sleep(0),意味着令 t 放弃 CPU 资源,等待操作系统重新调度。也相当于将执行机会让给其他线程。

五、线程状态

线程的状态是一个枚举类型 Thread.State

java 复制代码
public class getAllState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

NEW:线程对象已创建,但还没启动(start());

TERMINATED:线程执行完毕或因异常退出;

RUNNABLE:可执行,又分为 正在执行 和 等待操作系统资源(即包含了"就绪"和"运行"两种状态);

BLOCKED:线程在等待获取锁(synchronized)时被阻塞;

WAITING:线程无期限 等待另一个线程执行特定操作(wait()join() 等);

TIMED_WAITING:线程等待指定时间sleep()wait(timeout)join(timeout) 等)。


除了可以使用 线程对象.getState() 方法查看线程状态,还可以在 jconsole 中查看。在 jconsole 中还能查看线程具体的调用栈,尤其是线程处于阻塞状态时,可以查看阻塞的位置。

在多线程的程序中,理解线程状态,是帮助程序员调试程序(找 bug)的关键。

六、总结

本文介绍了Java线程的基础知识,主要包括:

  1. 线程的产生背景;

  2. Java线程与操作系统线程的关系;

  3. 创建线程的5种方法;

  4. Thread的常见属性和方法;

  5. 线程控制方法,如 isAlive() 判断存活状态、interrupt() 中断线程、join() 等待线程结束等;

  6. 线程调度特性,包括随机性和 sleep() 方法的注意事项

7.线程的6种状态

8.重点补充知识:回调函数、变量捕获

相关推荐
bu_shuo3 小时前
MATLAB图片的所有导出格式
开发语言·matlab·图片
东离与糖宝3 小时前
Spring AI Alibaba v1.0 正式版:Java 企业 AI 网关从 0 到 1 搭建
java·人工智能
学java的冲鸭3 小时前
【SpringAI第四章】函数调用
java·ai·springai
海参崴-3 小时前
C++ 位运算从入门到精通(全知识点+面试题+实战应用)
开发语言·c++
极创信息3 小时前
企业信创产品认证全流程:从信创适配到信创认证的实操指南(2026版)
java·数据库·spring boot·mysql·matlab·mybatis·软件工程
青岛少儿编程-王老师3 小时前
CCF编程能力等级认证GESP—C++1级—20260314
开发语言·c++
onebound_noah3 小时前
【实战解析】如何高效获取京东商品详情数据(含多语言SDK接入)
java·前端·数据库
重庆小透明3 小时前
【java基础内容】ArrayList与LinkedList的区别及ArrayList源码解析
java·开发语言·后端·面试·职场和发展
東雪木3 小时前
Java学习——重载 (Overload) 与重写 (Override) 的核心区别、底层实现规则
java·开发语言·jvm·学习·java面试