这里是Themberfue
· 在上一节中,我们讨论了死锁的概念,产生的场景 ,产生的必要条件......
内存可见性
· 我们先来看一段百度百科关于 "内存可见性" 的解释
· "内存可见性" 就是造成线程安全问题的原因之一
· 如果是单纯地看这段文字,可能会比较难读,我们通过一段代码来深入理解这个问题
public class Demo21 {public static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flag = 1;System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}
· 这段不难读懂,t1 线程判断变量 flag 的改变情况,若为 0,则 t1 线程循环执行,若被修改后,则 t1 线程会立刻终止执行
· t2 线程执行修改变量 flag 的任务,但并非立刻执行修改的逻辑,而是1ms 后
· 这样的代码理论上并无问题,在 1s 后,两个线程都会结束
· 但实际运行后,你会发现虽然 t2 线程结束了,但是 t1 线程却迟迟没有结束,查看变量 flag 的值,也确实更改为了 1,但为什么 t1 线程没有结束呢?
· 众所周知,程序员之间的水平都是参差不平的
· 优化代码这件事本来就是很微妙的,程序都能跑,那我就懒得优化了,这绝对是大多数程序员的写照(但是,代码优化也是非常重要的事情,还是要注意的)
· 研究 JDK 的大佬们,就希望通过 编译器 或者 JVM 来给我们写出的代码自动优化,在对逻辑不变的前提下,对代码的结构进行一些小幅度的调整从而使代码更加高效的运行
· 这样的事看起来当然好,但是难免地会引发弊端,尤其是在编写多线程代码时,编译器 或者 JVM 的优化可能就会判断失误,导致逻辑理论上是正确的,但结果却大不尽人如意
· "内存可见性" 就是由于 JVM 自动优化代码所造成的
· 我们先来了解汇编语言中的一个指令:"cmp" 本质是比较(compare)指令,通常用于控制程序流程(例如条件判断和分支),cmp 本质是一个隐式减法操作
while (flag == 0) {}
· 上述伪代码就是一个条件判断,在执行 "cmp" 指令之前,程序还得先执行 load 指令(在x86架构中是用 mov 指令执行类似的操作),该指令将内存中的数据加载到寄存器里
· 随后在执行 cmp 指令,所以 cmp 指令本质是纯 寄存器 操作的,而 load 指令涉及到内存的操作
· 回到最开始的代码,在 t1 线程运行后,while循环里的条件判断疯狂反复执行,判断变量 flag 是否变化了,所以在短时间内,其会执行几百上千次
· 在这个过程中,JVM 发现我始终从 内存 中读取到的值都是不变的,所以 JVM 就不从内存上读了,直接从寄存器上读了
· 因为相比于单纯地寄存器操作,内存操作的时间开销是非常大的,所以 JVM 就会帮助我们 "优化" 代码,后续 t2 线程修改内存中的变量 flag 值后,t1 线程也不会察觉到,这边就是产生 bug 的原因
· 如果稍微调整代码,让这个 while 循环里的逻辑加上 sleep(1)
public class Demo21 {public static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flag = 1;System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();} }
· 运行代码后,惊人地发现,这个问题解决啦!程序正常结束了!
· 这是为什么呢?
计算机的指令级操作一般都是纳秒级别的,本来程序是飞快执行 while 循环,但突然插入了一个 1ms 的等待,此时,JVM 对优化变量 flag 就感觉无足轻重了,自然程序就正常结束了
· 那这是否意味着这种问题以后都这样解决吗?
不是的,使用 sleep 会大大影响程序的运行效率,所以非常不推荐这样做
volatile
· 为了解决上述问题,Java5 引入了 volatile 关键字
· 通过这个关键字修饰的变量,就不会被编译器优化成单纯从读寄存器读取了
public class Demo22 {// 通过 volatile 关键字来修饰变量,那么该变量就不会被JVM优化操作了// 该关键字只能修饰变量public volatile static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}flag = 1;System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();} }
· 这样问题才真正得以解决
JMM
· JMM(Java Memory Model):Java内存模型
· 其是一种抽象的概念,为了更好的管理 Java 对内存的管理而定义出的一套定义和规则。虽然说编程语言可以直接地复用操作系统层面的内存模型,但是,不同的操作系统的内存模型一般都是不同的,这不符合 Java 跨平台的特性
· 于是,JMM就出现了,不仅抽象了线程与主内存之间的关系,还规定了 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,主要目的是简化多线程编程,提高程序的可移植性
· 最主要的还是:每个线程,有一个自己的 "工作内存" ,同时这些线程又共享一个 "主内存"
· 当一个线程循环执行上述的读取变量的操作时,就会先从 "主内存" 中的数据读取到 该线程的 "工作内存"当中
· 后续另一个线程修改时,也是想修改自己 "工作内存" 的数据,再拷贝到 "主内存" 中
· JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性
· 这里我就不深入讲解了,想要了解更多,可以参考下面的文章
JMM 详解
wait | notify | notifyAll
· 在之前的多线程编程中,两个或多个线程之间的并发操作都是随机的,我们虽然可以通过 sleep方法,join方法,加锁操作对线程执行的顺序或多或少的进行一些人为地控制
· 但这些方法通常考虑的都不会那么全面,尽管人为地进行了控制,但是其等待还是不确定的
· wait(等待) 和 notify(通知) 就是协调线程之间的执行逻辑的顺序的
· 其可以让后执行的逻辑等待先执行的逻辑,虽然无法直接干预调度器的调度顺序,但是可以让后执行的线程的某一段逻辑先 "等待",先执行的线程的某一逻辑执行完毕后,再 "通知" 先前等待的线程继续执行后续的逻辑
· wait 和 notify 都是 Object 提供的方法,也就是说,任何对象都可以调用这两个方法。就如同任何对象都可以当作锁对象一样
object.wait();
· 如果你直接使用上述代码,程序便会抛出一个 IllegalMonitorStateException 异常。为什么会抛出这个异常?
· 首先,wait方法执行后会先释放这个线程加的锁,再让其阻塞等待。在释放锁的前提下应该处于加锁状态,才能释放
· 你都没有加锁,谈何释放?为什么要先释放锁?很简单的一个问题:占着茅坑不拉屎~~~
· 虽然 wait 后先释放锁,但是后续被其他线程 notify 唤醒后,这个线程后续的执行逻辑又会重新加上锁,保证线程安全
· 虽然说 notify 不需要先释放锁在调用 notify,但是 Java 规定其必须先加锁后才可以使用 notify。操作系统中,也有相关的 api 接口,但是其 notify 不需要和锁搭配使用
public class Demo23 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {try {synchronized (locker) {System.out.println("wait之前");locker.wait();System.out.println("wait之后");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();Scanner sc = new Scanner(System.in);System.out.println("输入任意内容,唤醒t1");sc.next();// 必须保证 notify 在 wait 之前,不然唤醒个寂寞synchronized (locker) {locker.notify();}}
}
· 这四处都必须使用同一个锁对象,否则其不会生效
· 就好比这个锁对象是这两个线程之间沟通的 "桥梁"
· 必须保证 notify 在 wait 之前,不然唤醒个寂寞
· 如果有多个线程被同一个锁调用 wait 等待时,使用 notifyAll 可以唤醒所有这些等待的线程,所有被选择的线程从等待状态转移到阻塞状态,直到当前线程释放锁
public class Demo24 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {try {synchronized (locker) {System.out.println("t1 等待");locker.wait();System.out.println("t1 被唤醒");}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(() -> {try {synchronized (locker) {System.out.println("t2 等待");locker.wait();System.out.println("t2 被唤醒");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();Thread.sleep(100);// 针对多个线程都在同一个对象上 wait,notify 每次只会随机唤醒一个线程// notifyAll 可以唤醒全部线程synchronized (locker) {locker.notifyAll();}}
}
PS:如果有多个线程针对一个锁对象进行 wait,那么另一个线程对该锁对象进行 notify 只会唤醒随机一个线程
· wait 和 join 类似,也提供可以提供指定时间的等待:
public class Demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {try {synchronized (locker) {System.out.println("t1等待");locker.wait(5000);System.out.println("超过5秒,t1被唤醒");}} catch (InterruptedException e) {throw new RuntimeException();}});t1.start();Scanner sc = new Scanner(System.in);System.out.println("输入任意内容,唤醒 t1");sc.next();synchronized (locker) {locker.notify();}} }
· 加上时间的 wait 和 sleep 看似没有区别,都是指定时间的等待
· 实则还是有点区别的:
1. wait 必须搭配锁使用,必须先加锁才可以使用,而 sleep 不需要
2. 如果都是在 synchronized 代码块触发,wait 则需要先释放锁,再阻塞等待,然而 sleep 不会释放锁,而是依然占用着锁等待,这显然是不合理的
多线程小案例
题目:
有三个线程,分别只能打印 A,B,C,要求按顺序打印出 ABC,打印 10 次
public class Demo26 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Thread t1 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker1) {locker1.wait();}System.out.print('A');synchronized (locker2) {locker2.notify();}}} catch (InterruptedException e) {throw new RuntimeException();}});Thread t2 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker2) {locker2.wait();}System.out.print('B');synchronized (locker3) {locker3.notify();}}} catch (InterruptedException e) {throw new RuntimeException();}});Thread t3 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker3) {locker3.wait();}System.out.println('C');synchronized (locker1) {locker1.notify();}}} catch (InterruptedException e) {throw new RuntimeException();}});t1.start();t2.start();t3.start();Thread.sleep(100);synchronized (locker1) {locker1.notify();}} }
· 巧妙利用 wait notify 可以简单快速解决这个问题
· 毕竟不知后事如何,且听下回分解~~