目录
什么是线程?
一个线程就是一个 "执行流",每个线程之间都可以按照顺序执行自己的代码,多个线程之间 "同时" 执行着多份代码
相比于进程而言,线程更轻量
创建 线程 比创建 进程 更快
销毁 线程 比销毁 进程 更快
调度 线程 比调度 进程 更快
线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 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 | 休眠当前线程,但精度更高 |