【大家好,我是爱干饭的猿,本文是多线程初级入门,主要介绍了线程、Thread类及常用方法、线程状态、JVM区域划分、线程安全、synchronized 锁、JUC下的Lock 锁、volatile 关键字、wait 和 notify 用法。
后续会继续分享多线程单例模式、阻塞队列、定时器、线程池、多线程面试考点及其他重要知识点总结,如果喜欢这篇文章,点个赞👍,关注一下吧,】
上一篇文章:《【python入门】基础语法》
🤞目录🤞
7. 什么情况下会出现线程调度(开始选择一个新的线程分配cpu)
2. 前台线程vs后台线程/精灵线程(daemon)/守护线程
2. 得到当前引用对象Thread.currentThread();
2. 多核环境下,并发排序的耗时<串行排序的耗时(我们现在看到的现象)
2. synchronized在有限程度上可以保证内存可见性
3. synchronized 也可以给代码重排序增加一定的约束
7.1 volatile 90%的功能就是保护内存可见性的编辑
1. 认识线程
1.1 概念
1. 什么是线程(Thread)
线程就是一个 “执行流” ,每个线程都能执行自己的代码,多个线程可以 ”同时“ 执行多份代码。
2. 进程(process)和线程(Thread)的关系
进程:线程 = 1:m
- 一个线程一定属于一个进程;一个进程下可以有多个线程
- 一个进程下至少有一个线程,通常这个一开始就存在的线程,称为主线程,主线程和其他线程之间是完全平等的,没有如何特殊性
- 进程和进程之间不共享内存空间,同一个进程的线程之间共享同一个内存空间
- 进程是资源分配的基本单位,线程是os进行调度的单位
- 由于进程把调度单位这一个职责让渡给线程了,所以,使得单纯进程的创建销毁适当简单
- 由于线程的创建和销毁不涉及资源分配、回收的问题,所以,通常理解,线程的创建/销毁成本要低于进程的成本
3. 为什么os要引入 Thread 概念
由于进程这一概念天生就是资源隔离的,所以进程之间进行数据通信是个高成本工作
但现实中,一个任务通常需要多个执行流一起配合工作完成,所以就需要一种方便数据通信的执行流概念,线程就承担了这个职责。
所以线程变成了独立执行流的承载概念,而进程退化成资源(不包含cup)分配的承载概念
4. Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念,操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使 用(例如 Linux 的 pthread 库)。
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装。
面试:它是API为什么不直接调用啊,你说封装是封装了什么?(血的教训)
主要是操作系统线程还是有一些特性,和Java线程表现不一样,最典型的,比如说操作系统,一崩整个进程崩了,Java中就不是这样的,其次呢,封装就可以增加基本jvm对现成的控制嘛,具体有哪些,这个就太多了。
1.2 创建一个线程
1. java 线程在代码是如何体现
java.lang.Thread 类(包括其子类)的一个对象
2. 基本创建方法
a. 通过继承Thread 类,并且重写run 方法,
实例化该类对象 -> Thread 对象
public class Main { static class MyThread extends Thread{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } } public static void main(String[] args) { MyThread t1 = new MyThread(); t1.start(); } }
b. 通过实现Runnable 接口,并且重写run方法
实例化Runnable 对象,利用Runnable 对象构建一个Thread
public class Main { static class MyThreadTask implements Runnable{ @Override public void run() { System.out.println("我是子线程,实现Runnable接口"); } } public static void main(String[] args) { MyThreadTask task = new MyThreadTask(); // 创建了一个任务对象 Thread t2 = new Thread(task); // 把 task 作为 Thread 的构造方法传入 t2.start(); } }
c. 其他变形
- 匿名内部类创建 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 子类对象"); });
3. 启动线程
调用当前Thread 对象的start() 方法
注意:
- 一个已经调用过start()不能再调用start() 了再调用就会有异常发生,t.start() 只允许在”新建“ 状态下执行,执行两次,报IllegalThreadStateException 非法线程状态异常
- 不要调用成run(),如果调用run(),只是调用了一个类方法,并不是启动该线程
4. 如何理解t.start() 做了什么?
线程状态:新建 -> 就绪
t.start() 把线程的状态从新建变成了就绪状态,但并不分配cpu,只是该线程被加入到线程调度器的就绪队列中,等待被调度器选择分配cpu
从子线程进入就绪队列时,子线程和主线程地位就完全平等了,哪个线程被分配cpu都是随机的
5. 但大概率是主线程中的打印先执行,为什么?
因为子线程刚执行t.start(),立马被调度器分配cpu概率不大
6. 什么时候,子线程中的语句会先执行?
- 非常碰巧的在t.start()之后 sout(...)之前,发生了一次线程调度
- 主线程的状态运行–>就绪 主线程不再持有CPU。所以主线程的下一条语句不再执
- 调取时候,选中子线程调度 子线程的状态︰就绪–>运行,子线程持有了CPU,所以,执行到子线程的语句
7. 什么情况下会出现线程调度(开始选择一个新的线程分配cpu)
a. cpu 空闲
- 当前运行着的CPU执行结束了 运行->结束
- 当前运行着的CPU等待外部条件 运行->阻塞
- 当前运行着的CPU主动放弃 运行->就绪
b. 被调度器主动调用
- 高优先级线程抢占
- 时间片耗尽(最常见的情况)
在多线程中,明明代码是固定的,但会出现现象是随机的可能性,主要原因就是调度的随机性体现在线程的运行过程中! !
我们写的无论是Thread的子类还是 Runnable 的实现类,只是给线程启动的“程序”。所以,同一个程序,可以启动多个线程。
8. 线程 和 方法调用栈关系
每个线程都有自己独立的调用栈,debugger 中看到的每一行都是栈帧,保存的就是运行方法时的临时变量(主要就是局部变量)
编辑
如图是主线程的栈(先进先出)
这些框出现的顺序符合FILO(使用栈去维护这些框)
栈:当前执行流的当前时刻(时间停止状态时)的状态框有哪些(现实方法的调用次序)
框:栈帧(frame)装的就是运行该方法时需要的一些临时数据(主要就是具备变量)
编辑
目前jvm中运行的线程
因为每个线程都是独立的执行流,所以A线程和B线程调用的方法没有任何关系,表现就是每个线程有自己独立的栈
2. Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类
2.1 Thread 的常见构造方法
编辑
Thread t1 = new Thread(); Thread t2 = new Thread(new MyRunnable()); Thread t3 = new Thread("线程名字"); Thread t4 = new Thread(new MyRunnable(), "线程名字");
2.2 Thread 的几个常见属性
编辑
- ID 是线程的唯一标识,不同线程不会重复,只能get,不能set
- 名称是各种调试工具用到,便于开发者看,jvm本身并不需要这个属性,没设置名字,默认Thread-0、Thread-1...,可以getName(),setName()
- 状态表示线程当前所处的一个情况,下面我们会进一步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
1. 线程可以get/set自己的优先级
注意:这个优先级的设置,只是给JVM一些建议,不能强制让哪个线程先被调度。
2. 前台线程vs后台线程/精灵线程(daemon)/守护线程
后台线程一般是做一些支持工作的线程(JVM内部的管理线程)
前台线程一般是做一些有交互工作的 (我们创造的线程默认都是前台线程)
3. JVM进程什么时候才能退出
所有的前台线程都退出了,JVM进程就退出了
- 必须要求所有前台都退出,和主线程没关系
- 和后台线程没关系,即使后台线程还在工作,也正常退出
2.3 Thread 下几个常见的静态方法
1. 休眠Thread.sleep();
让线程休眠?毫秒
Thread.sleep(1000) == TimeUnit.SECOND.sleep(1) 让线程休眠1秒
从线程的状态的角度,调用sleep(?),就是让当前线程从 “运行”->“阻塞” ,当条件满足时(时间过去了) 线程从“阻塞”->"就绪" 这个间隔很短,基本对人类无感当线程被调度器选中时 开始接着之前的指令执行。
2. 得到当前引用对象Thread.currentThread();
Thread 引用,执行一个线程对象,执行的就是在哪个线程中调用的该方法,返回哪个线程对象。
2.4 中断一个线程-stop、-interrupt
1. 暴力停止 t.stop();
目前基本上已经不采用了。原因是直接杀掉B,不知道B是否把工作进行的如何了
2. 比较好的 t.interrupt();
编辑
A给B主动发一个信号,代表B已经停止了(发消息)
B在一段时间里,看到了停止信号之后,就可以主动,把手头的工作做到一个阶段完成,主动退出。
3. B如何感知到有人让它停止。
情况1: B正在正常执行代码,可以通过一个方法来判定 t.interrupted();
情况2: B可能正处于休眠状态(比如sleep、join),意味着B无法立即执行Thread.interrupted()
此刻,JVM的处理方式是,以异常形式,通知B, InterruptedException
public class Main { static class Mythread extends Thread{ @Override public void run() { while (true){ for (int i = 0; i < 1000; i++) { System.out.println("我正在写代码"); } if(interrupted()) { System.out.println("休息前停下来"); break; } try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { System.out.println("休息中停下来"); break; } if(interrupted()) { System.out.println("休息后停下来"); break; } } System.out.println("线程停下来了"); } } public static void main(String[] args) { Mythread t = new Mythread(); t.start(); Scanner scanner = new Scanner(System.in); String s = scanner.next(); t.interrupt(); } }
2.5 等待一个线程-join()
待一个线程完成它的工作后,才能进行自己的下一步工作
编辑
public class Main { private static class B extends Thread { @Override public void run() { // 模拟 B 要做很久的工作 try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } println("B 说:我的任务已经完成"); } } private static void println(String msg) { Date date = new Date(); DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); System.out.println(format.format(date) + ": " + msg); } public static void main(String[] args) throws InterruptedException { B b = new B(); b.start(); println("A 自己先去吃饭"); // 有 join 和没有 join 的区别 // 有join 时,b执行完了,才继续后面的指令 b.join(); println("A 说:B 给我把钱送来了,结账走人"); } }
1. 利用join 完成并发对一个数组进行归并排序
import java.util.Arrays; import java.util.Random; // 众人拾柴火焰高 public class ConcurrentSort { static class ArrayHelper { public static long[] generateArray(int n) { Random random = new Random(20220420); long[] array = new long[n]; for (int i = 0; i < n; i++) { array[i] = random.nextInt(); } return array; } } // 进行排序的线程 static class SortWorker extends Thread { private final long[] array; private final int fromIndex; private final int toIndex; // 利用构造方法,将待排序的数组区间情况,传入 // 对 array 的 [fromIndex, toIndex) 进行排序 SortWorker(long[] array, int fromIndex, int toIndex) { this.array = array; this.fromIndex = fromIndex; this.toIndex = toIndex; } @Override public void run() { // 具体的排序过程,这里使用 Array.sort 做模拟 Arrays.sort(array, fromIndex, toIndex); } } // 主线程记录排序耗时 public static void main(String[] args) throws InterruptedException { long[] array = ArrayHelper.generateArray(4_000_0000); // 分别是 // [0, 1000_0000) // [1000_0001, 2000_0000) // [2000_0001, 4000_0000) // [3000_0001, 4000_0000) long s = System.currentTimeMillis(); Thread t1 = new SortWorker(array, 0, 1000_0000); t1.start(); Thread t2 = new SortWorker(array, 1000_0001, 2000_0000); t2.start(); Thread t3 = new SortWorker(array, 2000_0001, 3000_0000); t3.start(); Thread t4 = new SortWorker(array, 3000_0001, 4000_0000); t4.start(); // 4 个线程开始分别的进行排序了 // 等待 4 个线程全部排序完毕 t1.join(); t2.join(); t3.join(); t4.join(); // 4 个线程一定全部结束了 // TODO:进行 4 路归并,将 4 个有序数组,归并成一个有序数组 long e = System.currentTimeMillis(); long elapsed = e - s; System.out.println(elapsed); } }
2. 多核环境下,并发排序的耗时<串行排序的耗时(我们现在看到的现象)
单线程一定能跑在一个CPU (核)上,多线程意味着可能工作在多个核上(核亲和性)
3. 单核环境下,并发排序的耗时也能小于么?
即使在单核环境下,并发的耗时也可能较少。
本身,计算机下就有很多线程在等待分配CPU,比如,现在有100个线程。意味公平的情况下,我们的排序主线程,只会被分配1/100的时间。 当并发时,我们使用4个线程分别排序,除其他的99个之外,计算机中共有99+4 = 103个线程我们4个线程同属于一个进程,分给我们进程的时间占比4/103 >1 / 100。
所以,即使单核情况下,我们一个进程中的线程越多,被分到的时间片是越多的。
4. 那线程越多越好么?
当然不是
- 创建线程本身也不是白嫖的,创建销毁线程都需要时间成本。
- 即使理想情况下,不考虑其他耗时,极限也就是100% 线程调度也需要耗时(OS从99个线程中挑一个的耗时和从9999个线程中挑一个的耗时不 。
CPU是公共资源,写程序的时候也是要考虑公德心的。如果是好的OS系统,可能也会避免这个问题
5. 并发排序的耗时就一定小于串行的么?
不一定。
串行的排序: t = t(排区间1) + t(排区间2)+ t(排区间3)+ t(排区间4)
并发的排序: t=4* t(创建线程)+t(排区间1)+ t(排区间2)+t(排区间3)+ t(排区间4)+4*t(销毁)
2.6 线程让出CPU-yield
线程从运行->就绪状态,随时可以继续被调度回CPU。
yield主要用于执行一些耗时较久的计算任务时,为让防止计算机处于“卡顿”的现象,时不时的让出一些CPU资源,给OS内的其他进程。
public class Main { static class MyThread extends Thread { private final String name; public MyThread(String name) { this.name = name; } @Override public void run() { while (true){ if(name.equals("张三")){ Thread.yield(); } System.out.println("我是"+name); } } } public static void main(String[] args) { MyThread 张三 = new MyThread("张三"); MyThread 李四 = new MyThread("李四"); 张三.start(); 李四.start(); } }
3. 线程的状态
3.1 观察线程的所有状态
线程的状态是一个枚举类型 Thread.State
public class Main { public static void main (String[]args){ for (Thread.State state : Thread.State.values()) { System.out.println(state); } } }
- NEW: 新建
- RUNNABLE: 就绪+运行
- BLOCKED: 这几个都表示排队等着其他事情(阻塞)
- WAITING: 这几个都表示排队等着其他事情(阻塞)
- TIMED_WAITING: 这几个都表示排队等着其他事情(阻塞)
- TERMINATED: 工作完成了
3.2 线程状态和状态转移的意义
编辑
在Java代码中看到的线程状态(只能获取不能设置,状态的变更是JVM控制的)
编辑
3.3 观察线程的状态和转移 jconsole工具
使用jconsole工具
C:\Program Files\Java\jdk1.8.0_131\bin
编辑
4. JVM下的内存区域划分
编辑
PC保存区(PC)、栈: 虚拟机栈本地方法栈、堆 方法区 运行时常量池
线程私有的区域:PC、栈(局部变量)
线程共享的内存区域:堆区(对象实例)、方法区(静态属性)、运行时常量池区(被加载的类)
1 为什么每个线程都得有自己的PC
每个线程都是独立的执行流,下一条要执行的指令和其他线程无关。所以有自己的PC
2 为什么每个线程都得有自己的栈
每个线程都是独立的执行流,有各自调用的方法链,有各自要处理的临时数据,所以栈也是独一份的。
5. 线程安全
5.1 线程安全的概念
代码的运行结果应该是100%符合预期!!
5.2 线程不安全的原因
1. 修改共享数据
1.站在开发者的角度
1)多个线程之间操作同一块数据了(共享数据)——不仅仅是内存数据
2)至少有一个线程在修改这块共享数据
多个线程中至少有一个对共享数据做修改(写)操作
即使在多线程的代码中,哪些情况下不需要考虑线程安全问题?
1.几个线程之间互相没有任何数据共享的情况下,天生是线程安全的;
2.几个线程之间即使有共享数据,但都是做读操作,没有写操作时,也是天生线程安全的。
2.系统角度解释
前置知识:
1. java代码(高级语言)中的一条语句,很可能对应的多条指令
r++实质就是r = r + 1
变成指令动作:
- 从内存中(r代表的内存区域)把数据加载到寄存器中 LOAD_A
- 完成数据加1的操作 ADD 1
- 把寄存器中的值,写回到内存中(r代表的内存区域) STORE_A
2. 线程调度是可能发生在任意时刻的,但是不会切割指令(一条指令只有执行完/完全没有执行两种可能)
3. 原子性
程序员的预期是r++ 或者r-- 是一个原子性的操作(全部完成or全部没完成)
但实际执行起来,保证不了原子性,所以会出错。
原子性被破坏是线程不安全的最常见的原因!!
4. 可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到
前置知识: CPU中为了提升数据获取速度,一般在CPU中设置缓存(Cache)
指令的执行速度>>内存的读写速度
编辑编辑
线程的所有的数据操作(读/写)必须:
1.从主内存加载到工作内存中
2.在工作内存中进行处理 ...允许在工作内存中处理很久
3.完成最终的处理之后,再把数据同步回主内存
1.把r从主内存->当前线程的工作内存中
⒉循环r++,完成1000次(在工作内存中完成)中间允许同步回主内存
3.在1000放回r(主内存)
内存可见性: 一个线程对数据的操作,很可能其他线程的无法感知的。甚至,某些情况下,会被优化成完全看不到的结果!
5. 代码重排序
所谓的重排序,就是指:执行的指令不和书写指令并不一致。
编辑
我们的写程序,往往是经过中间很多环节优化的结果,并不保证最终执行的语言和我们写的语句是一摸一样的。
原本的顺序可能是1->2->3 重排序变为1->3->2,都是在这1->3 之后如果有线程调用,就会出现线程不安全。
5.3 线程不安全问题
1.所谓什么是线程安全?
1.程序的线程安全:运行结果100%符合预期(这个标准无法实操,只是为了理解)
2.Java语境下,经常说某个类、对象是线程安全的: 这个类、对象的代码中已经考虑了处理多线程的问题了,如果只是“简单”使用,可以不考虑线程安全的问题。 ArrayList就不是线程安全的。——ArrayList实现中,完全没考虑过线程安全的任何问题。 无法直接使用在多线程环境(多个线程同时操作同一个ArrayList)
2.作为程序员如何考虑线程安全的问题?
1.尽可能让几个线程之间不做数据共享,各干各的。就不需要考虑线程安全问题了 比如上节课的归并排序:4个线程虽然处理的是同一个数组,但提前划好范围,各做各的,就没问题了
2.如果非要有共享操作,尽可能不去修改,而是只读操作 static final int COUNT =..;即使多个线程同时使用这个COUNT也无所谓的
3.一定会出现线程问题了,问题的原因从系统角度讲:
1.原子性被破坏了
2.由于内存可见性问题,导致某些线程读取到“脏(dirty) "
3.由于代码重排序导致的线程之间关于数据的配合出问题了 所以,接下来需要学习一些机制,目标和JVM和沟通,避免上述问题的发生
3. 线程安全的类和线程不安全的类
线程不安全: ArrayList、LinkedList、PriorityQueue、TreeMap、TreeSet、HashMap、HashSet、StrinaBuilder
线程安全: Vector、Stack、Dictionary、StringBuffer这几个类都是Java 设计失败的产品。 以后大家代码中不要出现这些类。
4. 最常见的违反原子性的场景?
1.read-write场景
i++;
array[size] = e;
size++;
2.check-update场景
if (a == 10) { a = ...; }
6. synchronized 锁
同步锁/monitor锁(监视器锁)
6.1 synchronized 的特性
1. 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
2. 刷新内存
synchronized 的工作过程:
1. 获得互斥锁
2. 从主内存拷贝变量的最新副本到工作的内存
3. 执行代码
4. 将更改后的共享变量的值刷新到主内存
5. 释放互斥锁
3. 可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
6.2 synchronized 使用示例
public class Main { static Object o = new Object(); static class MyThread1 extends Thread{ @Override public void run() { synchronized(o) { for (int i = 0; i < 100000; i++) { System.out.println("张三"); } } } } static class MyThread2 extends Thread{ @Override public void run() { synchronized(o) { for (int i = 0; i < 100000; i++) { System.out.println("李四"); } } } } public static void main(String[] args) { MyThread1 t1 = new MyThread1(); MyThread2 t2 = new MyThread2(); t1.start(); t2.start(); } }
编辑
锁理论上,就是一段数据(一段被多个线程之间共享的数据)
当多个线程:
1)都有加锁操作时
2)并且申请的是同一把锁时会造成,加锁 代码s 解锁
互斥的必要条件:线程都有加锁操作&&锁的是同一个对象
6.3 正确使用 synchronized
加锁操作使得互斥(synchronized和我们一起配合(我们需要正确地使用synchronized) )
1.保证了临界区的原子性
加锁粒度(临界区的大小)
编辑
粒度不是越粗越好也不是越细越好。
最好值是一个需要工程测量的取值
2. synchronized在有限程度上可以保证内存可见性
编辑
3. synchronized 也可以给代码重排序增加一定的约束
编辑
6.4 正确使用JUC包下的Lock锁
synchronized是一种非常早期就存在的锁
后期重新进行过一波设计,以类、对象的形式给我们使用(不在语言层面)java.util.concurrenJUC包 <--现代写java并发编程尽量使用这个包下提供的工具
编辑
编辑
public class Main { static int r = 0; static class MyThread1 extends Thread{ private Lock o; MyThread1(){} MyThread1(Lock o){ this.o = o; } @Override public void run() { o.lock(); try { for (int i = 0; i < 1000_0000; i++) { r++; } } finally { o.unlock(); } } } static class MyThread2 extends Thread{ private Lock o; MyThread2(){} MyThread2(Lock o){ this.o = o; } @Override public void run() { o.lock(); try { for (int i = 0; i < 1000_0000; i++) { r--; } } finally { o.unlock(); } } } public static void main(String[] args) throws InterruptedException { Lock o = new ReentrantLock(); MyThread1 t1 = new MyThread1(o); MyThread2 t2 = new MyThread2(o); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(r); } }
编辑
编辑
7. volatile 关键字
修饰变量,JVM中线程要读变量,每次从主内存读,写入,保证写回主内存。
7.1 volatile 90%的功能就是保护内存可见性的
编辑
7.2 保证代码重排序
对象的初始化过过程
1.构造代码块惇⒉属性的初始化赋值⒉.构造方法
保证 1 -> 2 -> 3
public class Main { volatile static boolean flag = false; static class MyThread extends Thread{ @Override public void run() { long r = 0; while (flag == false){ r++; } System.out.println(r); } } public static void main(String[] args) throws InterruptedException { MyThread t = new MyThread(); t.start(); TimeUnit.SECONDS.sleep(2); flag = true; } }
8. wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。
1.wait()和notify()方法是属于Object类的,Java 中的对象都带有这两个方法
⒉.要使用wait和notify,必须首先对“对象”进行synchronized加锁
8.1 wait()方法
wait 做的事情
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁
wait 结束等待的条件
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常
8.2 notify()方法
notify 方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其 它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行 完,也就是退出同步代码块之后才会释放对象锁。
import java.util.concurrent.TimeUnit; public class Demo1 { static class MyThread extends Thread { private Object o; MyThread(Object o) { this.o = o; } @Override public void run() { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o) { System.out.println("唤醒主线程"); o.notify(); } } } public static void main(String[] args) throws InterruptedException { Object o = new Object(); synchronized (o) { MyThread t = new MyThread(o); t.start(); o.wait(); // 1. wait会自己释放锁 2. 等待.. 3. 再加锁 System.out.println("永远不会到达"); } } }
8.3 notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程
import java.util.concurrent.TimeUnit; public class Demo3 { static Object o = new Object(); static class MyThread extends Thread { @Override public void run() { synchronized (o) { try { o.wait(); System.out.println(getName()); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; i++) { MyThread t = new MyThread(); t.start(); } // 保证了子线程们先 wait,主线程就先休眠一会儿 TimeUnit.SECONDS.sleep(5); synchronized (o) { // o.notify(); // 唤醒所有o的锁 o.notifyAll(); } } }
8.4 wait 和 sleep 的对比(面试题)
其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞 一段时间,唯一的相同点就是都可以让线程放弃执行一段时间
1. wait 需要搭配 synchronized 使用. sleep 不需要.
2. wait 是 Object 的方法 sleep 是 Thread 的静态方法
分享到此,感谢大家观看!!!
如果你喜欢这篇文章,请点赞加关注吧,或者如果你对文章有什么困惑,可以私信我。
🏓🏓🏓