一、Java内存模型
JMM即Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM 体现在以下几个方面
- 原子性:保证指令不会受到线程上下文切换的影响
- 可见性:保证指令不会受 cpu 缓存的影响
- 有序性:保证指令不会受 cpu 指令并行优化的影响
简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
二、不可见性导致的问题
示例
main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
import lombok.extern.slf4j.Slf4j; import static site.weiyikai.concurrent.utils.Sleeper.sleep; /** * Created by xiaowei * Date 2022/10/27 * Description 退不出的循环 */ @Slf4j(topic = "c.Test06") public class Test06 { static boolean run = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (run){ } }); t.start(); sleep(1); log.debug("停止t线程..."); run = false; // 线程t不会如预想的停下来 } }
运行结果
从结果可以看出,当run变量为false后,t线程并未退出循环。
问题分析
1、初始状态,t线程刚开始从主内存读取了run 的值到工作内存。
2、因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存 中,减少对主存中run的访问,提高效率
3、1秒之后, main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变 量的值,结果永远是旧值
三、可见性
3.1 volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。
import lombok.extern.slf4j.Slf4j; import static site.weiyikai.concurrent.utils.Sleeper.sleep; /** * Created by xiaowei * Date 2022/10/27 * Description 退不出的循环 -- volatile解决办法 */ @Slf4j(topic = "c.Test06") public class Test06 { volatile static boolean run = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (run){ } }); t.start(); sleep(1); log.debug("停止t线程..."); run = false; } }
运行结果
21:49:15.174 c.Test06 [main] - 停止t线程...
线程t在1s后正常退出了循环。
分析
当主线程修改主存中的run变量的时候,t线程一直访问的是自己缓存的run值,所以不认为run已经改为false,顾不会退出循环。
当为主存(成员变量)进行volatile修饰,增加变量的可见性, 当主线程修改run为false, t线程对run的值可见,这样就可以正常退出循环。
3.2 synchronized的可见性
synchronized的执行内部代码的过程分为五步,分别是:
- 1、获得同步锁;
- 2、清空工作内存;
- 3、在主内存中拷贝最新变量的副本到工作内存;
- 4、执行代码(计算或者输出等);
- 5、将更改后的共享变量的值刷新到主内存中;
- 6、 释放同步锁。
import lombok.extern.slf4j.Slf4j; import static site.weiyikai.concurrent.utils.Sleeper.sleep; /** * Created by xiaowei * Date 2022/10/27 * Description 退不出的循环 -- synchronized解决办法 */ @Slf4j(topic = "c.Test07") public class Test07 { static boolean run = true; private final static Object lock = new Object(); public static void main(String[] args) { Thread t = new Thread(() -> { while (run) { synchronized (lock) { if (!run){ break; } } } }); t.start(); sleep(1); log.debug("停止t线程..."); synchronized (lock){ run = false; } } }
运行结果
22:09:18.867 c.Test07 [main] - 停止t线程...
3.3 print打印输出
当在while循环代码中加入print打印输出时,t线程会退出循环。
import lombok.extern.slf4j.Slf4j; import static site.weiyikai.concurrent.utils.Sleeper.sleep; /** * Created by xiaowei * Date 2022/10/27 * Description 退不出的循环 -- print打印输出 */ @Slf4j(topic = "c.Test06") public class Test06 { static boolean run = true; public static void main(String[] args) { Thread t = new Thread(() -> { while (run){ System.out.print(""); } }); t.start(); sleep(1); log.debug("停止t线程..."); run = false; // 线程t不会如预想的停下来 } }
运行结果
22:10:28.651 c.Test06 [main] - 停止t线程...
分析
从print的源码中可以看出,print方法使用到了synchronized
,synchronized可以保证原子性、可见性、有序性。当使用synchronized后,就会将线程的工作内存清空,在主内存中拷贝最新变量的副本到工作内存。所以会获取到正确的run值。
四、可见性 vs 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线 程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。
上例从字节码理解是这样的:
getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true putstatic run // 线程 main 修改 run 为 false, 仅此一次 getstatic run // 线程 t 获取 run false
比较一下之前讲线程安全时举的例子:两个线程一个 i++
一个 i- -
,只能保证看到最新值,不能解决指令交错。
// 假设i的初始值为0 getstatic i // 线程2-获取静态变量i的值 线程内i=0 getstatic i // 线程1-获取静态变量i的值 线程内i=0 iconst_1 // 线程1-准备常量1 iadd // 线程1-自增 线程内i=1 putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 iconst_1 // 线程2-准备常量1 isub // 线程2-自减 线程内i=-1 putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。
总结
volatile关键字可以保证可见性、有序性,但是不能保证原子性。volatile具体原理见:volatile原理
synchronized关键字可以保证可见性、原子性,对于有序性:使用synchronized并不能解决有序性
问题,但是如果是该变量
整个都在synchronized代码块的保护范围内,那么变量就不会被多个线程同时操作,也不用考虑有序性问题!在这种情况下相当于解决了重排序问题!。