文章目录
- 1. 认识线程
- 1.1 概念
- 1.1.1 线程是什么
- 1.1.2 为什么要有线程
- 1.1.3 进程和线程的区别
- 1.1.4 Java的线程和操作系统线程的关系
- 1.2 第一个多线程程序
- 1.3 创建线程
- 1.4 多线程的优势
- 2. Thread 类及其常用的方法
- 2.1 Thread 的常见构造方法
- 2.2 Thread 的几个常见属性
- 2.3 启动一个线程 - start()
- 2.4 中断一个线程
- 2.5 等待一个线程 - join()
- 2.6 获取当前线程引用
- 2.7 休眠当前线程
1. 认识线程
1.1 概念
1.1.1 线程是什么
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
一个线程就是一个"执行流", 每个线程可以按照顺序执行自己的代码, 多个线程之间"同时"执行着多份代码
1.1.2 为什么要有线程
- 单核CPU的发展到了瓶颈, 要再想提高计算机算力的话, 就需要多核CPU, 而并发编程便能充分利用多核CPU资源
- 有些任务场景需要"等待IO"为了让"等待IO"的时间能去做其他的事情, 也需要用到并发编程
虽然多进程也能实现并发编程, 但是多线程要比多进程更轻量
- 创建线程比创建进程更快
- 销毁线程比销毁进程更快
- 调度线程比调度进程更快
1.1.3 进程和线程的区别
- 进程是包含线程的, 每个进程必须包含一个线程, 就是主线程
- 进程和进程之间是不共享内存空间的, 同一个进程的线程之间共享内存空间
- 进程是系统分配资源的最小单位, 线程是系统调度的最小单位
1.1.4 Java的线程和操作系统线程的关系
线程是操作系统中的概念, 操作系统内核实现了这样的机制,并且对用户层提供了一些 API 供用户使
用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
1.2 第一个多线程程序
下面我们写一个简单的多线程程序, 先不关心它是怎么写的, 就先简单的了解一下多线程,
感受多线程程序和单线程程序的区别
- 每个线程是一个独立的执行流
- 多个线程之间是"并发"执行的
package Thread;import java.util.Random;public class ThreadDemo1 {private static class MyThread extends Thread {@Overridepublic void run() {Random random = new Random();while (true) {// 打印线程的名称System.out.println(Thread.currentThread().getName());try {// 随机停止运行 0 ~ 9 秒Thread.sleep(random.nextInt(10));} catch (InterruptedException e) {throw new RuntimeException(e);}}}}public static void main(String[] args) {MyThread thread1 = new MyThread();MyThread thread2 = new MyThread();MyThread thread3 = new MyThread();// 启动线程thread1.start();thread2.start();thread3.start();Random random = new Random();while (true) {System.out.println(Thread.currentThread().getName());try {// 随机停止运行 0 ~ 9 秒Thread.sleep(random.nextInt(10));} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
下面我们用jconsole观察线程
首先我们打开jconsole
1.3 创建线程
- 继承Thread 类来创建一个线程
public class ThreadDemo2 {// 1.继承一个Thread 类来创建一个线程类class MyThread extends Thread {@Override// 重写里面的run方法public void run() {System.out.println("这里是线程运行的代码");}}public static void main(String[] args) {// 2.创建一个 MyThread 实例MyThread thread1 = new MyThread();// 3. 调用 start 方法启动线程thread1.start();}
}
- 实现 Runnable 接口
public class ThreadDemo3 {// 1. 实现Runnable接口static class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("这里是线程执行的代码");}}public static void main(String[] args) {// 2. 创建Thread类实例, 调用Thread类的构造方法时将Runnable对象作为target 参数Thread thread1 = new Thread(new MyRunnable());// 启动线程thread1.start();}
}
这里我们要注意的是:
实现Runnable接口,并不能直接启动或者说实现一个线程,Runnable接口和线程是两个不同的概念
换句话说,一个类,实现Runnable接口,这个类可以做很多事情,不仅仅只被用于线程,也可以用于其他功能!
- 继承 Thread 类直接使用 this 就表示当前线程对象的引用
- 而实现 Runnable 接口, this 表示的是 MyRunnable 的引用, 需要使用 Thread.currentThread() 表示当前线程对象的引用
- 使用匿名内部类创建 Thread 子类对象
public class ThreadDemo5 {public static void main(String[] args) {Thread thread1 = new Thread() {@Overridepublic void run() {System.out.println("使用匿名内部类创建 Thread 子类对象");}};//启动线程thread1.start();}
}
- 匿名内部类创建 Runnable 子类对象
public class ThreadDemo6 {public static void main(String[] args) {Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {System.out.println("使用匿名内部类创建 Runnable 子类对象");}});// 启动线程thread1.start();}
}
- lambda 表达式创建 Runnable 子类对象
public class ThreadDemo7 {public static void main(String[] args) {Thread thread1 = new Thread(() -> {System.out.println("使用匿名内部类创建 Thread 对象");});// 启动线程thread1.start();}
}
注:
Thread 类中run()和start()方法的区别
作用功能不同:
run方法的作用是描述线程具体要执行的任务;
start方法的作用是真正的去申请系统线程
运行结果不同:
run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。
1.4 多线程的优势
多线程的主要优势之一就是增加运行速度
下面我们举个例子具体看
我们使用并发和串行方式计算 a 和 b 的值, 分别让 a 和 b 自加20_0000_0000次,
/*** @describe* @author chenhongfei* @version 1.0* @date 2023/9/23*/
package Thread;import java.util.Random;
import java.util.concurrent.CountDownLatch;public class ThreadDemo8 {private static final long count = 20_0000_0000;public static void main(String[] args) throws InterruptedException {// 使用并行方式concurrency();// 使用串行方式serial();}// 并行private static void concurrency() throws InterruptedException {long begin = System.nanoTime();// 利用一个线程计算 a 的值Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (int i = 0; i < count; i++) {a++;}}});//启动线程thread1.start();// 主线程内计算b的值int b = 0;for (int i = 0; i < count; i++) {b++;}// 等待Thread线程结束thread1.join();long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 /1000; //1 纳秒=1e-6 毫秒System.out.printf("并发: %f 毫秒%n",ms);}// 串行private static void serial() {// 全在主线程内计算a, b 的值long begin = System.nanoTime();int a = 0;int b = 0;for (int i = 0; i < count; i++) {a++;}for (int i = 0; i < count; i++) {b++;}long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 /1000; //1 纳秒=1e-6 毫秒System.out.printf("串行: %f 毫秒%n",ms);}
}
2. Thread 类及其常用的方法
Thread类是Java中用于创建线程的类。线程是程序执行的最小单元,它允许多个任务并发执行,提高了程序的效率。Thread类的实例表示一个独立的执行线程,可以通过继承Thread类或实现Runnable接口来创建线程。Thread类提供了一系列方法来管理线程的状态和行为,比如启动线程、暂停线程、恢复线程、等待线程完成等。在Java中,线程的使用非常普遍,比如在网络编程、多线程服务器、GUI应用程序等领域都会用到线程。
2.1 Thread 的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread 的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriorty() |
是否后台线程 | isDaemom() |
是否存活 | isAlive() |
是否被中断 | isInterrupt() |
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
具体情况我们参考下面代码
/*** @describe* @author chenhongfei* @version 1.0* @date 2023/9/23*/
package Thread;public class ThreadDemo9 {public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().getName() + "我还活着");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + "我即将死去");});System.out.println(Thread.currentThread().getName() + ": ID" + thread1.getId());System.out.println(Thread.currentThread().getName() + ": 名称" + thread1.getName());System.out.println(Thread.currentThread().getName() + ": 状态" + thread1.getState());System.out.println(Thread.currentThread().getName() + ": 优先级" + thread1.getPriority());System.out.println(Thread.currentThread().getName() + ": 后台线程" + thread1.isDaemon());System.out.println(Thread.currentThread().getName() + ": 活着" + thread1.isAlive());System.out.println(Thread.currentThread().getName() + ": 被中断" + thread1.isInterrupted());//启动线程thread1.start();while (thread1.isAlive()) {System.out.println(Thread.currentThread().getName() + ": 状态" + thread1.getState());}}
}
2.3 启动一个线程 - start()
在Java中,start()方法通常用于启动线程。它是定义在Thread类中的方法,用于启动线程。
当调用start()方法时,线程会从它的run()方法开始执行。run()方法是线程的执行体,它包含了线程要执行的代码。
当线程启动后,它将从run()方法开始执行,直到该方法结束或线程被中止。在执行期间,线程可以访问共享变量和对象,并且可以与其他线程并发执行。
- 调用 start() 方法, 才真的在操作系统的底层创建出一个线程.
- 重写run() 方法是给线程提供具体的指令清单
Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 线程执行的代码(重写run 方法)System.out.println("Thread started.");}
});
thread.start(); // 启动线程
在上面的示例中,我们创建了一个新的线程对象,将一个实现了Runnable接口的对象传递给了构造函数。在run()方法中,我们打印了一条消息表示线程已经启动。然后,我们调用start()方法来启动线程。
需要注意的是,start()方法并不会阻塞调用它的线程。启动线程后,程序会继续执行其他代码。如果需要等待线程执行结束后再继续执行当前线程,可以使用join()方法。
2.4 中断一个线程
一个线程一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那该如何通知这个线程停止呢?这就涉及到我们的停止线程的方式了。
下面我们介绍两种方式:
- 1.通过共享的标记来进行沟通
通过设置一个标志位,线程在执行时可以检查这个标志位,如果发现标志位被设置了,那么线程就认为自己被中断了。
- 2.调用interrupt() 方法来通知
下面是一个简单的使用标志位中断线程的实例:
public class ThreadDemo10 {private static volatile boolean isInterrupted = false; //使用volatile 修饰, 保证线程间的可见性static Thread thread1 = new Thread(() -> {while (!isInterrupted) {System.out.println(Thread.currentThread().getName() + "开始转账");try {Thread.sleep(1000); // 模拟任务耗时} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + "取消转账, 差点误了大事");});public static void main(String[] args) {thread1.start(); // 线程启动, 开始转账isInterrupted = true; // 取消转账}
}
我们也可以使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
下面先介绍几个方法
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static booleaninterrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public booleanisInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
public class ThreadDemo11 {static Thread thread1 = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) { // Thread.currentThread().isInterrupted() 这就是标志位System.out.println(Thread.currentThread().getName() + "开始转账");try {Thread.sleep(1000); // 模拟任务耗时} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + "取消转账, 差点误了大事");});public static void main(String[] args) {thread1.start(); // 线程启动, 开始转账thread1.interrupt(); //设置标志位 取消转账}
}
线程收到通知的方法有两种:
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
- 否则, 只是内部的一个中断标志被设置,thread 可以通过:
Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
标志位就相当于一个开关
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了, 这个称为"清楚标志位"
Thread.currentThread().isInterrupted() 相当于按下开关, 开关不弹起来, 这个称为"不清除标志位"
- 使用Thread.isInterrupted() 线程中断, 会清除标志位
public class ThreadDemo12 {public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println(Thread.interrupted());try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});thread1.start();thread1.interrupt(); // 通知中断线程, 设置标志位}
}
只有第一个是 true , 后面都是 false 因为"开关弹回去了"
- 使用Thread.currentThread().Interrupted() 线程中断, 标志位不会清除
public class ThreadDemo13 {public static void main(String[] args) {Thread thread = new Thread(() -> {for (int i = 0; i < 10; i++) {System.out.println(Thread.currentThread().isInterrupted());}});thread.start();thread.interrupt();}
}
全部都是 true , 因为标志位不会清除
2.5 等待一个线程 - join()
有时候, 我们经常需要等待一个线程完成它的工作后, 才能进行下一步工作, 比如说, 这个月发工资我要买个手机, 只有等到了工资到账, 才能去买手机, 这时候我们就需要明确等待线程的结束.
每个Thread实例都有一个join()方法,该方法使得当前正在执行的线程暂停执行(阻塞),直到被join的线程执行结束(即从run()方法返回)。
public class ThreadDemo14 {public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("还有" + (5-i) +"天发工资");}System.out.println("工资已到账");});Thread thread2 = new Thread(() -> {System.out.println("正在去买手机的路上");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("买到了手机");});thread1.start();thread1.join(); //直到 thread1 执行结束才执行后面内容thread2.start();thread2.join();System.out.println("此任务结束");}
}
在这个例子中,主线程(Main Thread)会等待thread1线程执行结束,然后再执行自己的后续操作(让thread2执行)。join()方法的使用使得主线程能够同步地执行与子线程的结束。
如果把两个join() 注释掉, 效果如下:
此外还可以使用wait(), notify() 和 notifyAll(), 这个后面我们细说.
2.6 获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class ThreadDemo {public static void main(String[] args) {Thread thread = Thread.currentThread();}
}
currentThread() 现在就是指向当前执行线程的一个引用。你可以用这个引用来获取当前线程的各种信息,例如它的名称、它的优先级、它是否是守护线程等等。
System.out.println("Current thread name: " + currentThread.getName());
System.out.println("Current thread priority: " + currentThread.getPriority());
System.out.println("Is current thread a daemon?: " + currentThread.isDaemon());
2.7 休眠当前线程
我们可以使用Thread.sleep()方法来使当前线程休眠(暂时停止执行)一段时间。这个方法接受一个以毫秒为单位的时间参数,指定线程应该休眠的时间。
- 因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public class ThreadDemo15 {public static void main(String[] args) {Thread thread1 = new Thread(() -> {System.out.println(System.currentTimeMillis());try {Thread.sleep(3 * 1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(System.currentTimeMillis());});thread1.start();}
}