【多线程】Thread类及常用方法

目录

[一、Java中的 Thread 类 与 操作系统线程 的关系](#一、Java中的 Thread 类 与 操作系统线程 的关系)

二、多线程程序

如何启动线程?

[2.1 第一个多线程程序:继承自 Thread 类的子类](#2.1 第一个多线程程序:继承自 Thread 类的子类)

[2.2 实现 Runnable 接口并重写run方法](#2.2 实现 Runnable 接口并重写run方法)

[2.3 利用匿名内部类创建线程](#2.3 利用匿名内部类创建线程)

[2.4 匿名内部类解耦合创建线程](#2.4 匿名内部类解耦合创建线程)

[2.5 通过 Lambda 表达式创建线程](#2.5 通过 Lambda 表达式创建线程)

[三、Thread 类的常见方法](#三、Thread 类的常见方法)

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

[3.2 Thread 类常见属性](#3.2 Thread 类常见属性)

[3.2.1 isDaemon 方法](#3.2.1 isDaemon 方法)

[3.2.2 isAlive 方法](#3.2.2 isAlive 方法)

[3.2.3 终止线程的方法](#3.2.3 终止线程的方法)


一、Java中的 Thread 类 与 操作系统线程 的关系

线程是操作系统中的概念。操作系统内核实现了线程这种机制,并对用户层提供了一些 API (Application Programming Interface)供用户使用。

但是,操作系统提供的原生线程API是C语言的,因此,Java对操作系统提供的API进行了封装并将其导入标准库(java.lang.)中供用户使用,这就是Thread类。

二、多线程程序

如何启动线程?

使用 start () 方法在操作系统层面创建线程并开始执行。

注意,创建 Thread 类对象并不意味着类对象所对应的在操作系统中的线程也被创建。

2.1 第一个多线程程序:继承自 Thread 类的子类

在Thread类中,run方法是可以被重写的,以供线程按照程序员自己规定的逻辑运行。

java 复制代码
// 继承Thread类的子类,并重写run方法
class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("This is MyThread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo01_subclass {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();      // 创建线程
        
        // 主线程
        while (true) {
            System.out.println("This is main");
            Thread.sleep(1000);
        }
    }
}

在上面的代码中,我们通过创建继承Thread类的子类MyThread来创建一个线程并运行,与此同时,main方法中的主线程也在运行,这就形成了一个"并发"执行的多线程程序。

这里使用sleep语句时的异常处理使用 try...catch语句而不是用throws语句的原因是父类Thread的run方法并没有使用throws,因此只能使用 try...catch语句来处理异常。

执行代码之后,发现MyThread类和main两个内部的循环都在进行中,也就是说,主线程main和线程t是"并发"执行的。

可以看到,控制台输出的看似是main线程先执行然后再执行t线程,实际上操作系统对线程的调度采用"抢占式执行",其顺序是随机的,而这种随机调度是无法被程序员所干预的。

我们可以通过第三方工具(java进程)来观察进程的详细情况:

通过这个程序我们可以连接正在IDE运行的程序中进程的详细情况

这里看到t线程被自动命名为 "Thread-0" 了,实际上Thread类是可以自定义线程名称的,以便程序员辨认自己写的线程。当程序员没有提供自定义的线程名称时,JVM就会自动给线程按照顺序命名。

2.2 实现 Runnable 接口并重写run方法

创建线程的写法不止有通过子类这一种方法,还可以通过实现Runnable接口来创建线程。

java 复制代码
// 实现runnable接口,解耦合
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("This is Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Demo02_runnable {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();      // 创建线程

        // 主线程
        while (true) {
            System.out.printf("This is main");
            Thread.sleep(1000);
        }
    }
}

我们实现的Runnable类中重写了run方法,将线程所要执行的任务放在run方法中,而Thread类则只负责创建线程,当将来线程要执行的任务发生改变时直接修改runnable中的ren方法即可。

这种方法是是一种 解耦合 方法。

在代码中,我们一般希望 高内聚,低耦合,这样代码的可维护性就会比较高(较方便修改)。

(高内聚:在一个模块之内,把有关联的事物放在一块,比如功能相类似的函数等;低耦合:每个模块之间的影响和依赖尽可能的小)

2.3 利用匿名内部类创建线程

我们这次在 main 方法中创建一个匿名内部类,并在其中重写 run 方法:

java 复制代码
public class Demo03_anoInnerClass {
    public static void main(String[] args) throws InterruptedException {
        // 创建对象的时候使用了Thread的匿名(一次性)子类,并重写run方法
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("This is Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t.start();      // 创建线程

        // 主线程
        while (true) {
            System.out.println("This is main");
            Thread.sleep(1000);
        }
    }
}

这样就可以少定义一些类。

2.4 匿名内部类解耦合创建线程

考虑到降低耦合,我们可以做出一些调整:实现一个 Runnable 的匿名内部类并创建实例,然后再将 runnable 作为参数传给 Thread 创建线程对象,以实现解耦合。

java 复制代码
public class Demo04_anoInnerClassRunnable {
    public static void main(String[] args) throws InterruptedException {
        // 在Runnable这里创建匿名内部类,将任务和线程执行分离
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("This is Thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        Thread t = new Thread(runnable);
        t.start();      // 创建线程

        // 主线程
        while (true) {
            System.out.println("This is main");
            Thread.sleep(1000);
        }
    }
}

2.5 通过 Lambda 表达式创建线程

lambda表达式本质是一个"回调函数",通过实现函数式接口创建子类和对应的实例(编译器自动重写了方法),这样不仅简洁、清晰,还可以利用编译器加快效率。

java 复制代码
public class Demo05_lambda {
    public static void main(String[] args) throws InterruptedException {
        // 使用lambda表达式
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("This is Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();      // 创建线程

        // 主线程
        while (true) {
            System.out.println("This is main");
            Thread.sleep(1000);
        }
    }
}

三、Thread 类的常见方法

在Java中,Thread 类是 JVM 专门用来管理线程的一个类。

Thread 类对象是用来描述一个线程执行流的,每一个线程都有唯一的 Thread 类对象与之对应,而 JVM 会将这些 Thread 类对象组织起来用于线程调度和线程管理。

3.1 Thread 类常见构造方法

构造方法 说明
Thread () 创建线程对象,需要重写 run 方法
Thread (Runnable runnable) 使用 Runnable 对象创建线程对象,不需要重写 run 方法
Thread (String name) 创建线程对象并命名
Thread (Runnable runnable, String name) 使用 Runnable 对象创建线程对象并命名
【了解】Thread (ThreadGroup group, Runnable runnable) 用于对线程进行分组管理
  1. Thread t1 = new Thread();
  2. Thread t2 = new Thread(new MyRunnable());
  3. Thread t3 = new Thread("MyThread");
  4. Thread t4 = new Thread(new MyRunnable(), "MyThread");

3.2 Thread 类常见属性

属性 说明 获取方法
ID ID是线程的唯一标识,不可重复 getId ()
名称 名称方便程序员进行调试 getName ()
状态 状态表示线程当前所处的情况 getState ()
优先级 优先级高的线程理论上更易被调度执行 getPriority ()
是否为后台线程 JVM 在一个进程的所有非后台线程结束之后才停止执行 isDaemon ()
是否存活 当前线程是否执行结束 isAlive ()
是否被中断 终止线程 isInterrupted ()

3.2.1 isDaemon 方法

后台线程也可以理解为"守护线程",这些"守护线程"只会默默地在非后台线程运行的时候陪伴:

  • 当非后台线程全部结束,他们也随之结束;
  • 当非后台线程没全部结束时,"守护线程"结不结束都不会影响非后台线程。

非后台线程其实就是我们所创建的线程(包括主线程),而后台线程则是 JVM 在执行进程时的一些自带线程,当进程结束(非后台线程全部结束),后台进程也就结束了。

当我们想将自己创建的线程改成后台线程,可以使用 setDaemon () 方法。

java 复制代码
public class Demo07_daemonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("thread t");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        // 使用 setDaemon 方法可以将我们创建的线程修改为后台线程
        // 但是需要在 start 方法之前使用
        t.setDaemon(true);
        t.start();

        // 主线程
        for (int i = 0; i < 3; i++) {
            System.out.println("main");
            Thread.sleep(1000);
        }
        // 线程 t 已被修改为后台线程,当主线程结束,线程 t 也随之结束
        System.out.println("main-结束");
    }
}

3.2.2 isAlive 方法

虽然 java 中,一个系统线程和 Thread 类对象存在一对一的对应关系,但是 Thread 类对象的生命周期和系统中线程的生命周期是不同的。

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

        System.out.println(t.isAlive());
        // 当还未调用 start 方法时,线程 t 还没有被创建出来(但是 t 对象存在),输出的结果为 false

        t.start();
        // 当调用 start 方法之后才真正创建线程

        while (true) {
            System.out.println(t.isAlive());
            Thread.sleep(1000);
        }
        // 3秒过后线程 t 已被销毁,但是 t 对象还在(能够调用 isAlive 方法)
    }
}

3.2.3 终止线程的方法

  1. 我们可以自己定义一个布尔类型的变量作为线程是否中断的标志:
java 复制代码
class Test {
    public int value = 0;
}

public class Demo10_isFinishedFunc {
    private static boolean isFinished = false;

    public static void main(String[] args) throws InterruptedException {
        // 若 isFinished 定义在此处,会编译报错
        // 因为 lambda 是有"变量捕获"的------即使用自身所在类之外的变量
        // 此处的 isFinished 是局部变量,由于 lambda 是回调函数,在线程创建之后才执行
        // 等到 lambda 执行,此时主线程早已执行完毕,isFinished 已被销毁
        //boolean isFinished = false;
        
        // 将 isFinished 改成成员变量,就不会触发"变量捕获"语法

        Test test = new Test();

        Thread t = new Thread(() -> {
            while (!isFinished) {
                System.out.println("thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread-结束");
        });
        t.start();

        Thread.sleep(3000);
        isFinished = true;
    }
}
  1. 我们可以使用 interrupted 方法,Thread 类中含有一个布尔类型的变量充当线程是否中断的标志。
方法 说明
public void interrupt () 主动中断 Thread 类对象关联的线程,若线程处于阻塞状态,就以异常的方式通知,否则设置标志位
public static boolean interrupted () 判断当前线程的中断标志位,调用后清除标志位
public boolean isInterrupted () 判断对象关联的线程的标志位,调用后不清除标志位

需要注意 interrupted () 和 isInterrupted () 的区别:

| 特性 | interrupted() | isInterrupted() |
| 方法类型 | 静态方法 | 实例方法 |
| 检测对象 | 当前线程(类名调用) | 调用方法的线程对象 (对象调用) |
| 清除中断状态 | 是 | 否 |

典型场景 当前线程中断后需清除状态 监控其他线程或保留中断标志
  1. 当我们使用 interrupted () :

若线程因 wait/join/sleep 方法处于阻塞状态时,抛出 InterruptedException 异常通知并清除中断标志(当抛出异常后线程需要执行 catch 语句:可以抛出异常包装类异常终止线程;也可以 break 跳出循环正常终止线程)

  1. 当我们使用 isInterrupted () :

通过 Thread.currentThread().isInterrupted() 来判断线程的中断标志且不清除中断标志。(这种方式通知更及时)

java 复制代码
public class Demo11_isInterruptedFunc {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //while (t.isInterrupted())  -> 此处 lambda 中使用 t 的时机是在 Thread 创建 t 对象之前
            // 使用 currentThread 方法获取到当前线程的引用 t ,其作用相当于 this 关键字
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    // 1. 当主线程调用 interrupt 方法强制终止 t 线程,此时 t 线程正在 sleep,sleep 被中断就抛出异常
                    // 然后执行"throw new RuntimeException(e)"抛出包装异常,由于没有外层异常处理器(run方法没有try...catch处理异常语句),线程 t 异常终止
                    // 2. (空语句)
                    // 当什么都不写,t 线程不会被终止:当调用 interrupt 方法时 sleep 被提前唤醒并抛出异常,JVM 此时将终止标志改回 false
                    break;  // 3. 若添加 break 语句,当 sleep 在执行过程中被打断并抛出异常后,异常被 catch 语句捕获,跳出循环,不再检查终止标志(此时终止标志已被重置为 false)
                    // 程序将正常终止
                }
            }
            System.out.println("thread-结束");
        });
        t.start();

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

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=f5hn3bezp2r


相关推荐
小年糕是糕手3 小时前
【C++】内存管理(下)
java·c语言·开发语言·数据结构·c++·算法
CoderYanger3 小时前
第 479 场周赛Q2——3770. 可表示为连续质数和的最大质数
java·数据结构·算法·leetcode·职场和发展
L.EscaRC3 小时前
Spring Boot开发中加密数据的模糊搜索
java·spring boot·后端
艾莉丝努力练剑3 小时前
【Linux基础开发工具 (六)】Linux中的第一个系统程序——进度条Linux:详解回车、换行与缓冲区
java·linux·运维·服务器·c++·centos
8Qi83 小时前
Redis之Lua脚本与分布式锁改造
java·redis·分布式·lua
钱多多_qdd3 小时前
mini-spring基础篇:IoC(十一):Aware接口
java·spring
AM越.3 小时前
Java设计模式超详解——抽象工厂设计模式(含UML图)
java·设计模式·uml
嵌入式小能手3 小时前
飞凌嵌入式ElfBoard-文件I/O的深入学习之文件锁
java·服务器·学习