文章目录
- 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.不中断
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们 需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如 何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
1.通过共享的标记来进行沟通
2.调用 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 收到通知的方式有两种:
1.如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志
1.当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择 忽略这个异常, 也可以跳出循环结束线程.
2.否则,只是内部的一个中断标志被设置,thread 可以通过
1.Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志 false 变 true
2.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就回到非阻塞队列中继续运行。
感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手点一点赞,你的每一次鼓励都是作者创作的动力哦!😘