文章目录
- 1. 认识线程(Thread)
- 1.1 概念
- 1.2 第一个多线程程序
- 1.3 创建线程
- 1.3.1方法1 继承 Thread 类
- 1.3.2方法2 实现 `Runnable` 接口
- 1.4 多线程的优势-增加运行速度
- 1.5 PCB、PID、进程和线程之间的关系
- 2. Thread(/θred/) 类及常见方法
- 2.1 Thread 的常见构造方法
- 2.2 Thread 的几个常见属性
- 2.3 启动一个线程-start()
- 2.4 中断一个线程
- 2.5 等待一个线程-join()
- 2.6 获取当前线程引用
- 2.7 休眠当前线程
大家好,我是晓星航。今天为大家带来的是 多线程-初阶 相关的讲解!😀
1. 认识线程(Thread)
1.1 概念
1) 线程是什么
一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的业务。我们进一步设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
2) 为啥要有线程
首先, “并发编程” 成为 “刚需”.
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
- 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.(线程之所以轻,是因为把申请资源/释放资源的操作给省下了)
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.
3) 进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
比如之前的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他人取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是一家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
相当于我们只增加处理资源的线程,把申请资源和释放资源的操作省下来了!!!
如果我们进行多线程操作,相当于只有第一个线程启动的资源开销是比较大的,后续线程的加入就很简单了。同一个进程里的多个线程之间,共用了进程的同一份资源(主要指的是 内存 和 文件描述符表)。
注:一个线程只能在一个进程中,但是一个进程可以包含多个线程。
4) Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
如果一个线程抛异常,处理不好,很可能把其他线程都给带走了,导致所有线程都挂了。
1.2 第一个多线程程序
感受多线程程序和普通程序的区别:
- 每个线程都是一个独立的执行流
- 多个线程之间是 “并发” 执行的.
并行:微观上同一时刻,两个核心上的进程,就是同时执行的
并发:微观上,同一时刻,一个核心上只能运行一个进程。但是它能够对进程快速的进行切换,比如说 CPU 这个核心上,先运行一下 QQ音乐,再运行以下 cctalk ,再以下LOL,只要切换速度足够快(2.5GHz,每秒运行 25亿条指令),宏观上认识感知不到的
未来除非显式声明,否则谈到并发,就是指并行 +并发。
import java.util.Random;
public class ThreadDemo {
private static class MyThread extends Thread {
@Override
public 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) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
// 随机停止运行 0-9 秒
e.printStackTrace();
}
}
}
}
Thread-0
Thread-0
Thread-2
Thread-1
Thread-2
Thread-1
Thread-0
Thread-2
main
main
Thread-2
Thread-1
Thread-0
Thread-1
main
Thread-2
Thread-2
......
使用jconsole
命令观察线程
1.3 创建线程
1.3.1方法1 继承 Thread 类
- 继承 Thread 来创建一个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 MyThread 类的实例
MyThread t = new MyThread();
- 调用 start 方法启动线程
t.start(); // 线程开始运行
上述操作中有解耦合。
解耦合:目的就是为了让 线程 和 线程 要干的活之间分离开。未来如果要改代码,不用多线程,使用多进程,或者线程池,或者协程…此时代码改动比较小
1.3.2方法2 实现 Runnable
接口
- 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
- 调用 start 方法
t.start(); // 线程开始运行
对比上面两种方法:
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
其他变形
- 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
- 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
- lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
1.4 多线程的优势-增加运行速度
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
- 使用
System.nanoTime()
可以记录当前系统的 纳秒 级时间戳. serial
串行的完成一系列运算.concurrency
使用两个线程并行的完成同样的运算.
public class ThreadAdvantage {
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使用并发方式
concurrency();
// 使用串行方式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利用一个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运行结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串行: %f 毫秒%n", ms);
}
}
并发: 399.651856 毫秒
串行: 720.616911 毫秒
1.5 PCB、PID、进程和线程之间的关系
PCB 对应的是线程。
一个线程对应一个PCB。
一个进程对应多个PCB。
如果一个进程只有一个线程,就是一个进程对一个PCB了。
同一个进程里的若干PCB、PID相同,不同进程的 PID 是不同的。
2. Thread(/θred/) 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关 联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象 就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
注:我们使用Thread类时不必要import一个包,因为我们的Thread就再java.lang下面。
2.1 Thread 的常见构造方法
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
注:这里的3和4方法多出来的String name的作用是给我们的线程起名字。
2.2 Thread 的几个常见属性
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
前台线程:会阻止进程结束,前台线程的工作没做完,进程是不可结束的。
后台线程:不会阻止进程结束,后台线程工作没做完,进程也是可以结束的。
代码里手动创建的线程,默认都是前台的。包括 main
默认也是前台的。其他 jvm
自带的线程都是后台的,也可以手动的使用 setDaemon
设置成后台线程。是后台线程就是守护线程。
即isDaemon()返回为true 那么该线程就是后台线程。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
isAlive() 是在判断,当前系统里面的这个 线程 是不是真的有了。
另外,如果内核里线程把 run 干完了,此时线程销毁,pcb随之释放。但是 Thread t 这个对象还不一定被释放的。此时isAlive() 也是 false。(这个函数只关注内核里的线程是否在工作,不关注Thread所创建的对象是否还存在)
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0;i < 3;i++) {
System.out.println("hello");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"mythread");
t.start();
while (true) {
try {
Thread.sleep(1000);
System.out.println(t.isAlive());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
在执行完for的三次后,t被销毁,因此后续的isAlive()返回的都是false。
- 线程的中断问题,下面我们进一步说明
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还
活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());
System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
}
2.3 启动一个线程-start()
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程 就开始运行了。
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把 李四、王五叫过来了
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
调用 start 方法, 才真的在操作系统的底层创建出一个线程.
通过循环打印"hello world" 和 "hello thread"来观察两个线程是怎么工作的。
由上图可知"hello world" 和 "hello thread"这两个字符串循环打印。
注:这里的"hello world" 和 "hello thread"他们的打印顺序是随机的,内核里本身并非是随机的,但是干扰因素太多,并且应用程序这一层也无法感知到细节,就只能认为是随机的了!
如果把上述代码的t.start
改成t.run
那么会在run中出不来,相当于只有一个线程在干活!!!
C:\Program Files\Java\jdk1.8.0_192\bin
在这里我们可以找到jconsole
这个查看进程的工具
找到我们idea中运行的这个进程
由于是我们自己的电脑,所以很安全不会存在不安全一说。
连接完选择线程这一类,我们就可以很清楚的看到我们thread中的所有线程。
被我们红方框圈出来的就是我们的调用栈,描述了当前方法之间的调用关系。
2.4 中断一个线程
我们线程中的中断不是让线程立即就停止,而是通知线程你应该要停止了。是否真的停止,取决于线程这里具体的代码写法。此时线程有三个选择:
1.立即中断
2.稍后中断
3.不中断
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们 需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如 何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
示例-1: 使用自定义的变量来作为标志位.
- 我们自定义falg为标志位,并在一开始设置为true
package thread;
public class ThreadDemo8 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (flag) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
//在主线程里就可以随时通过 flag 变量的取值,来操作 t 线程是否结束。
flag = false;
}
}
因为这里休眠3000毫秒后,flag变为false,因此我们的线程循环while中变为false而终止线程。
示例-2: 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定 义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
- 使用 thread 对象的 interrupted() 方法通知线程结束.
thread 收到通知的方式有两种:
-
如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志
- 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择 忽略这个异常, 也可以跳出循环结束线程.
-
否则,只是内部的一个中断标志被设置,thread 可以通过
- Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志 false 变 true
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志 中断标志为false
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
package thread;
public class ThreadDemo7 {
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) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();
}
}
这里interrupt在将线程内部的标志位(boolean)给设置为true,如果线程在进行sleep,就会触发异常,把sleep唤醒。
但是sleep在唤醒时,还会做一件事,把刚才设置的这个标志位,再设置回false。(清空了标志位)
这就导致了sleep的异常被catch完了之后,循环还要继续执行。
我们这里为大家提供了解决这个方法的三个情况:
1.
线程t忽略了你的终止请求。
2.
线程t立即响应你的终止请求
3.
稍后进行终止
唤醒之后线程到底要终止,还是要执行,到底是立即终止还是稍后,就把选择权交给程序猿自己了。
示例-3 观察标志位是否清除
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted()
相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”
Thread.currentThread().isInterrupted()
相当于按下开关之后, 开关弹不起来, 这个称为 “不清除标志位”.
- 使用
Thread.isInterrupted()
, 线程中断会清除标志位.
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
- 使用
Thread.currentThread().isInterrupted()
, 线程中断标记位不会清除.
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true
2.5 等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转 账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
大家可以试试如果把两个 join 注释掉,现象会是怎么样的呢?
附录
package thread;
public class ThreadDemo9 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
System.out.println("join 之前");
//此处的 join 就是让当前的 main 线程来等到 t 线程执行结束 (等待 t 的 run 执行完)
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("join 之后");
}
}
本身执行完start之后,t线程和main线程就并发执行,分头行动。
main继续往下执行,t也会继续往下执行。
遇到t.join()
就会发生阻塞
一直阻塞到,t线程结束,main线程才会从join中恢复过来,才能继续往下执行。(t线程肯定比main线程先结束)
如果开始执行join的时候已经结束了,join就不会阻塞,就会立即返回。
2.6 获取当前线程引用
这个方法我们以及非常熟悉了
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
在哪个线程中调用,就能获取到哪个线程的实例。
2.7 休眠当前线程
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实 际休眠时间是大于等于参数设置的休眠时间的。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
被sleep的PCB(线程)就相当于放到了阻塞队列中,我们程序继续运行非阻塞队列,当sleep的时间耗完时,我们的PCB就回到非阻塞队列中继续运行。
感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘