JVM之JMM

Java内存模型

java 内存模型

简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序
性、和原子性的规则和保障

原子性

问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作。

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
在这里插入图片描述
而对应 i-- 也是类似:
在这里插入图片描述
在这里插入图片描述
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
在这里插入图片描述
但多线程下这 8 行代码可能交错运行(为什么会交错?思考一下): 出现负数的情况:
在这里插入图片描述
出现正数的情况:
在这里插入图片描述

解决办法

在这里插入图片描述
用 synchronized 解决并发问题:
在这里插入图片描述
如何理解呢:你可以把 obj 想象成一个房间,线程 t1,t2 想象成两个人。
当线程 t1 执行到 synchronized(obj) 时就好比 t1 进入了这个房间,并反手锁住了门,在门内执行
count++ 代码。
这时候如果 t2 也运行到了 synchronized(obj) 时,它发现门被锁住了,只能在门外等待。
当 t1 执行完 synchronized{} 块内的代码,这时候才会解开门上的锁,从 obj 房间出来。t2 线程这时才
可以进入 obj 房间,反锁住门,执行它的 count-- 代码。

注意:上例中 t1 和 t2 线程必须用 synchronized 锁住同一个 obj 对象,如果 t1 锁住的是 m1 对象,t2 锁住的是 m2 对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果。

可见性

退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不见,导致了 t 线程无法停止:
在这里插入图片描述
为什么呢?分析一下:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
    在这里插入图片描述
    因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
    在这里插入图片描述
    1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述
解决办法

volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存

可见性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:
在这里插入图片描述
比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

在这里插入图片描述

注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低如果在前面示例的死循环中加入System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?

有序性

诡异的结果

在这里插入图片描述
I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?
有同学这么分析
情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过
了)
但我告诉你,结果还有可能是 0
相信很多人已经晕了
这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:
借助 java 并发压测工具 jcstress
在这里插入图片描述
创建 maven 项目,提供如下测试类
在这里插入图片描述
执行
在这里插入图片描述
在这里插入图片描述
可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。

解决方法

volatile 修饰的变量,可以禁用指令重排
在这里插入图片描述
结果为:
在这里插入图片描述

有序性理解

在这里插入图片描述
可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
在这里插入图片描述
也可以是
在这里插入图片描述
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性,例如著名的 double-checkedlocking 模式实现单例
在这里插入图片描述
以上的实现特点是:
1)懒惰实例化
2)首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
但在多线程环境下,上面的代码是有问题的, INSTANCE = new Singleton() 对应的字节码为:
在这里插入图片描述
这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

happens-before

happens-before 规定了哪些写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,
抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变
量的读可见

线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
在这里插入图片描述
线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
在这里插入图片描述
在这里插入图片描述
线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或t1.join()等待它结束)
在这里插入图片描述
线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或t2.isInterrupted)
在这里插入图片描述
对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值