一、Java内存模型 JMM即Java Memory Model,它定义了主存、工作内存抽象概念, 底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
JMM 体现在以下几个方面
原子性 :保证指令不会受到线程上下文切换的影响
可见性 :保证指令不会受 cpu 缓存的影响
有序性 :保证指令不会受 cpu 指令并行优化的影响
简单的说,JMM 定义了一套在多线程读写共享数据时 (成员变量、数组)时,对数据的可见性、有序性和原子性的规则和保障。
二、不可见性导致的问题 示例 main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import lombok.extern.slf4j.Slf4j;import static site.weiyikai.concurrent.utils.Sleeper.sleep;@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 ; } }
运行结果
从结果可以看出,当run变量为false后,t线程并未退出循环。
问题分析
1、初始状态,t线程刚开始从主内存读取了run 的值到工作内存。
2、因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存 中,减少对主存中run的访问,提高效率
3、1秒之后, main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变 量的值,结果永远是旧值
三、可见性 3.1 volatile(易变关键字) 它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import lombok.extern.slf4j.Slf4j;import static site.weiyikai.concurrent.utils.Sleeper.sleep;@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 ; } }
运行结果
1 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、 释放同步锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import lombok.extern.slf4j.Slf4j;import static site.weiyikai.concurrent.utils.Sleeper.sleep;@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 ; } } }
运行结果
1 22:09:18.867 c.Test07 [main] - 停止t线程...
3.3 print打印输出 当在while循环代码中加入print打印输出时,t线程会退出循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import lombok.extern .slf4j .Slf4j ;import static site.weiyikai .concurrent .utils .Sleeper .sleep ;@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 ; } }
运行结果
1 22:10:28.651 c.Test06 [main] - 停止t线程...
分析
从print的源码中可以看出,print方法使用到了synchronized
,synchronized可以保证原子性、可见性、有序性。当使用synchronized后,就会将线程的工作内存清空,在主内存中拷贝最新变量的副本到工作内存。所以会获取到正确的run值。
四、可见性 vs 原子性 前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线 程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况。
上例从字节码理解是这样的:
1 2 3 4 5 6 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- -
,只能保证看到最新值,不能解决指令交错。
1 2 3 4 5 6 7 8 9 10 11 getstatic i getstatic i iconst_1 iadd putstatic i iconst_1 isub putstatic i
synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低。
总结 volatile关键字可以保证可见性、有序性,但是不能保证原子性。volatile具体原理见:volatile原理
synchronized关键字可以保证可见性、原子性,对于有序性:使用synchronized并不能解决有序性
问题,但是如果是该变量
整个都在synchronized代码块的保护范围内 ,那么变量就不会被多个线程同时操作,也不用考虑有序性问题!在这种情况下相当于解决了重排序问题!。