Thread 类的基本用法

发布于:2024-08-14 ⋅ 阅读:(60) ⋅ 点赞:(0)

目录

什么是线程?

编写多线程程序

线程创建的方式

继承 Thread 类,重写 run 方法

实现 Runnable 接口,重写 run 方法

匿名内部类创建 Thread 子类

匿名内部类创建 Runnable 子类对象

lambda表达式

Thread 类和常用方法

Thread 的常见构造方法

Thread 的常见属性

启动线程

start 和 run 的区别

中断线程

等待线程

获取当前线程引用

休眠当前线程


什么是线程?

一个线程就是一个 "执行流",每个线程之间都可以按照顺序执行自己的代码,多个线程之间 "同时" 执行着多份代码

相比于进程而言,线程更轻量

创建 线程 比创建 进程 更快

销毁 线程 比销毁 进程 更快

调度 线程 比调度 进程 更快

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用(如 Linux 的 pthread 库)

而在 Java 标准库中,将这些 API 进行了封装,因此我们可以直接使用 Thread 类来实现多线程程序

编写多线程程序

要编写多线程程序,就需要使用 Thread 类,我们继承 Thread 类,并重写 run 方法

class MyThread extends Thread{
    @Override
    public void run() {
        // run 方法,线程的入口方法
        while (true) {
            System.out.println("thread");
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // 根据类创建出线程实例
        Thread thread = new MyThread();
        // 调用 Thread 的 start 方法,调用系统 API 在系统内核中创建出线程
        thread.start();
        while (true) {
            System.out.println("main");
        }
    }
}

我们在继承 Thread 时,发现这个类可以直接使用,不需要导包,这是为什么呢?

这是因为 Java 标准库中有一个特殊的包 java.lang,而使用 java.lang 包下的所有类,都不需要手动导入, Thread 类就在这个包中,因此不需要手动导包

run 方法的作用是什么呢?

main 方法是 一个 Java 程序的入口方法,而 run 方法的作用 与 main 方法类似,是该线程的入口方法

为什么选择重写 run 方法,而不是直接使用 Thread 类的 run 方法呢?

方法重写,本质上就是为了在现有的类的基础上进行扩展,实现一个线程, 就是想让这个线程执行实现我们需求的代码,但标准库自带的 run 方法并不知道我们的需求,要实现的业务逻辑需要我们手动指定,因此,我们就需要针对原有的 Thread 进行扩展

注意:不要忘了调用 start 方法,创建出线程

观察运行结果:

我们可以发现,此时两个循环都在执行,这也可以看出,这两个线程是两个独立的执行流,它们互不干扰,各自执行各自的代码

那么,是先打印 thread,还是先打印 main 呢? 

当有多个线程的时候,这些线程执行的先后顺序是不确定的,这是因为操作系统内核中实现的线程调度顺序是 随机调度 的,随机调度,也就意味着:

一个线程,什么时候被调度到 CPU 上执行,时机是不确定的

一个线程,什么时候从 CPU 被调度走,让其他线程执行,时机也是不确定的

从打印结果我们也可以看出,每次打印的 main / thread 的个数是不确定的

由于两个线程中都在执行死循环逻辑,而循环体只是单纯的打印,因此这两个循环执行的速度非常快,也就导致 CPU 占用率比较高,也就会进一步提高电脑的功耗

我们使用 Thread 提供的静态方法 sleep 来降低循环速度,让线程每打印一次,就休眠一段时间:


class MyThread extends Thread{
    @Override
    public void run() {
        // run 方法,线程的入口方法
        while (true) {
            System.out.println("thread");
            // 一次打印完成后,休眠 2s
            Thread.sleep(2000);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // 根据类创建出线程实例
        Thread thread = new MyThread();
        // 调用 Thread 的 start 方法,调用系统 API 在系统内核中创建出线程
        thread.start();
        while (true) {
            System.out.println("main");
            // 一次打印完成后,休眠 2s
            Thread.sleep(200);
        }
    }
}

在使用 sleep 方法时,会抛出一个 受查异常 InterruptedException

意味着在 sleep 2s 的过程中,该线程可能被提前唤醒

对异常进行处理:


class MyThread extends Thread{
    @Override
    public void run() {
        // run 方法,线程的入口方法
        while (true) {
            System.out.println("thread");
            // 一次打印完成后,休眠 2s
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        // 根据类创建出线程实例
        Thread thread = new MyThread();
        // 调用 Thread 的 start 方法,调用系统 API 在系统内核中创建出线程
        thread.start();
        while (true) {
            System.out.println("main");
            // 一次打印完成后,休眠 2s
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

在 MyThread 中,只能通过 try catch 处理异常,而不能通过 throws 抛出异常,这是为什么呢?

这是因为 MyThread 继承自 Thread,如果加上了 throws ,就修改了方法签名(父类 Thread 的 run 方法没有 throws 这个异常)此时也就不能构成重载了, 因此,子类在重写的时候,不能通过 throws 抛出异常

此时我们再次运行程序,打印的速度就会慢很多

上述代码中,我们通过 继承 Thread,重写 run 方法的方式,实现了线程的创建

但创建线程的方式不仅仅只要这一种,接下来,我们就来继续学习其他线程创建的方式

线程创建的方式

继承 Thread 类,重写 run 方法

也就是我们上述实现的方式,通过继承 Thread 来创建一个线程类

// 继承 Thread,重写 run 方法
class MyThread1 extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("继承 Thread 类,重写 run 方法");
        }
    }
}

创建 MyThread1 实例,并调用 start 方法启动线程

        Thread t1 = new MyThread1();
        t1.start();

实现 Runnable 接口,重写 run 方法

// 实现 Runnable,重写 run
class MyThread2 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("实现 Runnable 接口,重写 run 方法");
        }
    }
}

Runnable 可理解为 "可执行的",通过这个接口就可以抽象表示出一段可以被其他实体来执行的代码

由于 Runnable 表达的代码只是一段可以执行的代码,因此,在创建实例时,还是需要使用 Thread 类,才能真正在系统中创建出线程

通过实现 Runnable的方式来创建线程,将 线程 和 要执行的任务 进行了解耦合

创建 Thread 类实例,并调用 start 方法启动线程

        // 创建 Thread 类实例,调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
        Thread t2 = new Thread(new MyThread2());
        t2.start();

匿名内部类创建 Thread 子类

还是通过继承 Thread 的方式创建实例,但使用的是匿名内部类

        // 继承 Thread,重写 run,使用匿名内部类
        Thread t3 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("匿名内部类创建 Thread 子类");
                }
            }
        };
        t3.start();

匿名内部类创建 Runnable 子类对象

既然可以通过匿名内部类的方式创建 Thread 子类,也就可以通过匿名内部类的方式创建 Runnable 子类对象

        // 实现Runnable,重写 run,使用匿名内部类
        Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("匿名内部类创建 Runnable 子类对象");
                }
            }
        });
        t4.start();

在 Thread 构造方法的参数中填写 Runnable 的匿名内部类实例 

lambda表达式

        // 使用 lambda 表达式
        Thread t5 = new Thread(() -> {
            while (true) {
                System.out.println("lambda表达式");
            }
        });
        t5.start();

通过lambda表达式 创建 Runnable 子类对象,相当于 实现 Runnable 重写 run 方法

Thread 类是 JVM 用来管理线程的一个类,也就是说,每个线程都有一个唯一的 Thread 对象与之关联

每个执行流都需要一个对象来描述,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度和线程管理

接下来,我们来学习 Thread 类的重要属性和常用方法

Thread 类和常用方法

Thread 的常见构造方法

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

关于线程的名称,当我们自己创建线程,且未命名时,默认是按照 Thread-0、Thread-1、Thread-2.... 来对不同的线程命名的,且线程之间的名字是可以重复的,线程的名称对于线程的执行,没有太大的影响,对其进行命名,主要为了方便我们进行调试 

Thread 的常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否是守护(后台)线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

 ID:ID 是线程的唯一标识,是由 JVM 自动分配的身份标识,具有唯一性

名称:方便我们进行调试

状态:表示线程当前所处的状态(如 就绪状态 阻塞状态 等)

优先级:优先级高的线程理论上来说更容易被调度到,但在 Java中设置优先级,会对内核调度器的调度过程产生一些影响,效果不太明显(由于系统的随机调度)

守护(后台)线程:后台线程的运行,不会阻止进程结束

如何理解呢?

后台进程,也就有 前台线程

前台线程的运行,会阻止进程结束

后台线程的运行,不会阻止进程结束

我们通过一个具体的例子来进一步理解 前台线程 和 后台线程

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }
}

当 main 中执行到 thread.start() 时,main 线程已经结束了,但 t 仍然在继续执行,仍未结束,因此,此时的 thread 是一个 前台线程

我们将其设置为 后台线程 

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}

注意:要在 thread.start() 方法执行之前,也就是线程创建前,进行设置 

观察运行结果: 

此时,控制台还什么都没打印,进程就结束了

我们创建的线程,默认是前台线程,会阻止进程结束,只要前台线程没执行完,进程就不会结束(即使 main 已经执行完毕) 即,JWM 会在一个进程的所有前台线程结束后,才会结束运行

是否存活:表示内核中的线程是否还存在

Java中定义的 线程对象(Thread)实例,虽然表示一个线程,但这个对象本身的生命周期,和内核中的 PCB 生命周期,是不完全一样的

我们来看具体的例子:

public class ThreadDemo3 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("thread");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("start 之前:" + thread.isAlive());
        // 在 start 之前,设置 线程为后台线程
        thread.setDaemon(true);
        thread.start();
        System.out.println("start 之后:" + thread.isAlive());
        Thread.sleep(3000);
        // 3s 后,线程 thread 已经结束了
        System.out.println("thread 结束之后" + thread.isAlive());
    }
}

运行结果: 

当执行完 new Thread 时,此时 thread 对象已经创建好了,但是 内核 PCB 还没有,因此,isAlive 也就是 false

而当执行到 thread.start() 时,才真正在 内核 中创建出 PCB,此时的 isAlive 就是 true

当线程的 run 方法执行完毕时,此时这个内核中的线程就结束了(内核 PCB 就释放了)

但此时 t 变量还在,因此,isAlive 也就是 false

中断:关于线程的中断,我们后续再进行详细说明

启动线程

我们通过覆写 run 方法,创建一个线程对象,但线程对象被创建出来,并不意味着线程就开始运行了

覆写 run 方法是提供给线程要做的事情的指令清单,但当调用 start 方法时,才是真的在操作系统的底层创建出一个线程

Thread 类使用 start 方法启动一个线程,对于同一个 Thread 对象来说,start 方法只能调用一次:

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        
        t.start();
    }
}

 观察运行结果:

此时抛出 llegalThreadStateException 异常

调用 start 方法创建出新的线程,本质上是 start 调用系统的 API 来完成创建线程的操作

因此,若我们想要启动更多的线程,就需要创建新的对象

start 和 run 的区别

我们来看下面的代码:

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t.start();
        
        while (true) {
            System.out.println("main");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

运行结果:

此时控制台打印出 main 和 thread 说明,两个线程都在执行

但,当我们使用 t.run 方法时:

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

       // t.start();
        t.run();
        
        while (true) {
            System.out.println("main");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

运行结果:

 

可以看到,此时控制台只会打印出 thread,这是因为,此时调用 run 方法后,仍在 main 主线程中,并没有创建出新的线程,此时代码就停留在 run 方法的循环中,而下面 main 中的循环无法执行到

从上述例子我们就可以看出:

start 方法,用于创建出一个新的线程,当线程对象被创建出来时,并不意味着线程就开始运行了,只有当调用 start 方法时,才会真正在操作系统底层创建出一个线程

而 run 方法,是线程的入口方法,我们通过覆写 run 方法,实现我们的需求,告诉线程要做什么事情

中断线程

中断一个线程,即终止一个线程,让线程的 run 方法执行完毕,那么,该如何让线程提前终止呢?

其核心也就是让 run 方法能够提前结束

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        
    }
}

若我们想结束上述死循环,该如何实现呢?

我们可以引入一个 标志位

public class ThreadDemo6 {
    private static boolean isQuit = false;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程结束!");
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        isQuit = true;
        System.out.println("使用标志位让 t 线程结束");
    }
}

观察运行结果:

通过上述代码,就可以使线程结束掉,线程什么时候结束,取决于另一个线程中何时修改 isQuit 的值

当设置了 isQuit = true 后,是主线程先打印 "使用标志位让 t 线程结束",还是 t 线程先打印 "线程结束!“,是不确定的(由于线程的随机调度)

上述我们通过定义一个变量 isQuit 来实现结束线程

而 Thread 类中内置了这样的变量:

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束!");
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 使用 interrupt 方法修改标志位的值
        t.interrupt();
        System.out.println("使用标志位让 t 线程结束");
    }
}

Thread.currentThread():获取当前线程实例 t

isInterrupted():判定当前线程是否中断

t.interrupt():设置标志位,相当于之前的 isQuit = true

运行程序:

我们发现:当调用 interrupt 方法后,线程并没有结束,而是 抛出了 InterruptedException 异常,并被 catch 捕获,打印出了异常信息

这是因为在调用 interrupt 方法时, sleep 的休眠时间还没到,被提前唤醒了,此时就会抛出 InterruptedException 异常(然后就被 catch 捕获到),并且清除 Thread 对象的 isInterrupted 标志位

我们通过 interrupt 方法,将标志位设置为了 true,但由于 sleep 提前唤醒操作,此时就又把标志位设置为了 false,因此,循环还会继续执行

若我们不使用 sleep :

此时线程能够正常结束

若我们要使用 sleep 方法,且想要让线程结束,只需要在 catch 中加上 break 即可

因此,中断一个线程有两种常用的方式:

通过共享的标记来进行中断

调用 interrupt() 方法来通知

等待线程

由于线程的随机调度,抢占式执行,因此,多个线程的执行顺序是不确定的

虽然线程底层的调度是无序的,但我们可以在应用程序中,通过一些 API 来影响线程的执行顺序,其中 join 就是用来影响 线程结束 的先后顺序的方法

当我们需要一个线程等待另一个线程结束后,才能继续执行,此时就可以使用 join 等待另一个线程结束

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos) 精度更高的等待线程结束

使用 join 方法等待线程结束,那么,是谁等谁呢?

哪个线程中调用了 join 方法,就是该线程等待其他线程结束

public class ThreadDemo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("thread");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t 线程结束");
        });

        t.start();
        // 等待 t 线程结束
        t.join();
        System.out.println("main 线程结束");
    }
}

观察运行结果:

main 线程要等到 t 线程结束后,才会继续执行打印操作

在执行 join 方法时,若 t 线程正在运行,main 线程就会阻塞(暂时不参与 CPU 执行)

而当 t 线程运行结束后,main 线程就会从阻塞中恢复过来,并继续向下执行

通过 阻塞,就使得这两个线程的结束时间产生了先后顺序

sleep 也可以用来等待,那么,什么时候使用 sleep,什么时候使用 join呢?

我们来看下面这个例子:

在主线程中创建一个新的线程,由新线程完成一系列的运算,再由主线程负责获取到最终结果 

public class ThreadDemo8 {
    private static int result = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 1; i <= 100; i++) {
                result += i;
            }
        });

        t.start();
        // 此时,不知道 t 线程要执行多久,就可以使用 join 等待
        // join 会以 t 线程执行结束作为等待的条件
        // 什么时候 t 线程运行结束,join 就什么时候结束等待
        t.join();
        System.out.println("result: " + result);
    }
}

相比于 sleep(固定时间的等),join 则是固定任务的等,会等到其他线程任务完成后才继续往下执行

此时计算量并不大,因此运行速度很快,但若计算量很大时, t 线程进行运算,main 线程等待,此时的运算速度就会比较慢:

public class ThreadDemo8 {
    private static long result = 0;
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() -> {
            for (long i = 1; i <= 10_000_000L; i++) {
                result += i;
            }
        });
        long beginTime = System.currentTimeMillis();
        t.start();
        // 此时,不知道 t 线程要执行多久,就可以使用 join 等待
        // join 会以 t 线程执行结束作为等待的条件
        // 什么时候 t 线程运行结束,join 就什么时候结束等待
        t.join();
        long endTime = System.currentTimeMillis();
        System.out.println("result: " + result);
        System.out.println("time = " + (endTime - beginTime) + " ms");
    }
}

运行结果:

 

此时,我们可以再创建一个线程,来一起完成运算:

public class ThreadDemo9 {
    private static long result = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            long tmp = 0;
            for (long i = 1; i < 5_000_000L; i++) {
                tmp += i;
            }
            result += tmp;
        });

        Thread t2 = new Thread(() -> {
            long tmp = 0;
            for (long i = 5_000_000L; i <= 10_000_000L; i++) {
                tmp += i;
            }
            result += tmp;
        });
        long beginTime = System.currentTimeMillis();
        t1.start();
        t2.start();
        // 等待 t1 和 t2 线程计算完成
        t1.join();
        t2.join();
        long endTime = System.currentTimeMillis();
        System.out.println("result: " + result);
        System.out.println("time = " + (endTime - beginTime) + " ms");
    }
}

t1 线程计算前一半的结果,t2线程计算后一半的结果,main 线程等待 

运行结果: 

 

使用两个线程完成计算时,虽然时间不是使用一个线程计算时的 1/2,但也大幅度缩短了运行时间

当我们直接使用 join 时,就相当于是 死等,一定要等到其他线程执行完后才能继续向下执行,但使用 死等,很容易导致线程卡住,无法继续处理后续的逻辑

因此,我们可以为其设置一个 超时时间(等待上限时间),若等待的时间达到超时时间,就不再继续等,而是继续执行

获取当前线程引用

获取当前线程的引用,我们首先会想到使用 this

若是通过继承 Thread 的方式创建线程,则可以直接使用 this 拿到线程实例

但是,若是使用 Runnable 或 lambda 的方式创建线程,this 就不再指向 Thread 对象了,此时,就只能使用 currentThread()

我们在实现中断线程时就已经使用过了,通过 currentThread() 方法来返回当前对象的引用

方法 说明
public static Thread currentThread(); 返回当前线程对象的引用

休眠当前线程

我们在前面也使用过,通过 sleep 方法让线程进入休眠状态,但是,由于线程的调度是不可控的,因此,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

方法 说明

public static void sleep(long millis) throws InterruptedException

休眠当前线程

public static void sleep(long millis, int nanos) throws InterruptedException

休眠当前线程,但精度更高