jvm|【JVM】JVM08(java内存模型解析[JMM])
admin
2023-08-16 11:55:34
0次

??写在前面
  • 这里是温文艾尔の学习之路
  • 如果对你有帮助,给博主一个免费的点赞以示鼓励把QAQ
  • 博客主页 温文艾尔の学习小屋
  • ??更多文章?请关注温文艾尔主页
  • 文章发布日期:2022.02.08
  • java学习之路!
  • 欢迎各位点赞评论收藏??
  • 新年快乐朋友们
  • jvm学习之路!
  • ??上一篇内容:【JVM】JVM07(类加载阶段详细解析)

文章目录
  • 1.java内存模型
  • 1.1原子性
  • java内存模型中保证原子性的方法
  • 1.2可见性
  • 1.2.1可见性
  • 1.3有序性
  • 1.3.1解决方法
  • 1.3.2有序性的理解

1.java内存模型
java内存模型-Java Memory Model的意思,这个要和java内存结构进行区分
对于JMM的权威介绍,请参考
jmm介绍
简单的说,JMM定义了一套才多线程读写共享数据时(成员便来那个、数组)时,对数据的可见性、有序性和原子性的规则和保障

1.1原子性 java中对静态变量的自增,自减并不是原子操作
例如对i++而言(i为静态变量),实际会产生如下的JVM字节码指令:

getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //自增 putstatic i //将修改后的值存入静态变量

而对应i–也是类似:
getstatic i //获取静态变量i的值 iconst_1 //准备常量1 isub //自减 putstatic i //将修改后的值存入静态变量

【jvm|【JVM】JVM08(java内存模型解析[JMM])】而java的内存模型如下,完成静态变量的自增,自减需要在主存和内存之间进行数据交换
jvm|【JVM】JVM08(java内存模型解析[JMM])
文章图片

如果是单线程以上代码是顺序执行(不会交错),没有问题:

//i的初始值为0 getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //自增 线程内i=1 putstatic i //将修改后的值存入静态变量,静态变量i=1 getstatic i //获取静态变量i的值,线程内i=1 iconst_1 //准备常量1 isub //自减 线程内i=0 putstatic i //将修改后的值存入静态变量 静态变量i=0

在多线程下这几行代码可能会交错进行
出现负数的情况:

getstatic i //线程1-获取静态变量i的值,线程内i=0 getstatic i //线程2-获取静态变量i的值,线程内i=0 iconst_1 //线程1-准备常量1 iadd //线程1-自增 线程内i=1 putstatic i //线程1-将修改后的值存入静态变量,静态变量i=1 iconst_1 //线程2-准备常量1 iadd //线程2-自减 线程内i=-1 putstatic i //线程2-将修改后的值存入静态变量,静态变量i=-1

出现正数的情况:
getstatic i //线程1-获取静态变量i的值,线程内i=0 getstatic i //线程2-获取静态变量i的值,线程内i=0 iconst_1 //线程1-准备常量1 iadd //线程1-自增 线程内i=1 iconst_1 //线程2-准备常量1 iadd //线程2-自增 线程内i=-1 putstatic i //线程2-将修改后的值存入静态变量,静态变量i=-1 putstatic i //线程1-将修改后的值存入静态变量,静态变量i=1

java内存模型中保证原子性的方法 synchronized (同步关键字)
语法
synchronized (对象){ 要作为原子操作代码 }

用synchronized解决并发问题:
public class Demo01 { static int i = 0; static Object obj = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int j = 0; j < 5000; j++) { synchronized (obj){ i++; } } }); Thread t2 = new Thread(()->{ for (int j = 0; j < 5000; j++) { synchronized (obj){ i--; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }

i=0

可以想象Thread1和Thread2两个人,要执行的是一个房间,而object是一把锁,Thread1进入房间(执行)后房间上锁,Thread2要想进入房间除非Thread1出房间
注意上例中Thread1和Thread2必须用synchronized锁住同一个obj对象,如果ti锁住的是m1对象,t2锁住的是m2对象,就好比两个人分别进入了两个不同的房间,没法起到同步的效果
1.2可见性 先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:
public class Demo02 { static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while (run){ //... } }); t.start(); Thread.sleep(1000); run = false; } }

分析一下:
1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。
jvm|【JVM】JVM08(java内存模型解析[JMM])
文章图片

2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少主存中run的访问,提高效率
jvm|【JVM】JVM08(java内存模型解析[JMM])
文章图片

3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
jvm|【JVM】JVM08(java内存模型解析[JMM])
文章图片

解决办法
volatile(易变关键字)

他可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取值。线程操作volatile变量都是直接操作主存
public class Demo02 { static volatile boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ while (run){ //... } }); t.start(); Thread.sleep(1000); run = false; } }

jvm|【JVM】JVM08(java内存模型解析[JMM])
文章图片

可以看到1s之后程序结束了
1.2.1可见性
上面例子体现的实际就是可见性,他保证的是在多个线程之间,一个线程对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–
getstatic i //线程1-获取静态变量i的值,线程内i=0 getstatic i //线程2-获取静态变量i的值,线程内i=0 iconst_1 //线程1-准备常量1 iadd //线程1-自增 线程内i=1 putstatic i //线程1-将修改后的值存入静态变量,静态变量i=1 iconst_1 //线程2-准备常量1 iadd //线程2-自减 线程内i=-1 putstatic i //线程2-将修改后的值存入静态变量,静态变量i=-1

注意
synchronized语句块即可以保证代码块的原子性,也同时保证代码块内变量的可见性,但缺点是synchronized是属于重量级操作,性能相对更低
如果在前面示例的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程也能正确看到对run变量的修改了,想想为什么?
我们查看源码
public void println() { newLine(); }private void newLine() { try { synchronized (this) { ensureOpen(); textOut.newLine(); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } }

发现输出语句是加了synchronized锁
1.3有序性 执行下列代码
int num = 0; boolean ready = false; //线程1执行此方法 public void actor1(I_Result r){ if(ready){ r.r1 = num + num; }else{ r.r1 = 1; } }//线程2执行此方法 public void actor2(I_Result r){ num = 2; ready = true; }

I_Result是一个对象,有属性r1用来保存结果,那么可能的结果有几种呢?
除了结果为1,4的情况外,结果还有可能为0
这种情况为:
线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num=2
这种现象叫做指令重排,是JIT编译器在运行时做的一些优化

1.3.1解决方法 volatile修饰的变量可以禁用指令重排
1.3.2有序性的理解 同一个线程内。JVM会在不影响正确性的前提下,可以调整语句的执行顺序
static int i; static int j; //在某个线程内执行如下赋值操作 i = ...; //较为耗时的操作 j = ...;

可以看到至于是先执行i还是先执行j,对最终结果不会产生影响。所以上面代码真正执行时,即可以是
i = ...; //较为耗时的操作 j = ...;

也可以是
j = ...; i = ...; //较为耗时的操作

这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性,例如注定的double-checked locking模式实现单例
public class Singleton { public Singleton() {} private static Singleton INSTANCE = null; public static Singleton getInstance(){ //实例没创建,才会进入内部的synchronized代码块 if (INSTANCE == null){ synchronized (Singleton.class){ //也许有其他线程已经创建实例,所以再判断一次 if (INSTANCE == null){ INSTANCE = new Singleton(); } } } return INSTANCE; } }

以上的实现特点是:
  • 懒惰实例化
  • 首次使用getInstance()才使用synchronized加锁,后续使用时无需加锁
但在多线程环境下,上面的代码还是有问题的,INSTANCE = new Singleton(); 对应的字节码为:
new#2 dup invokespecial #3 putstatic #4

其中4 7两步的顺序是不固定的,也许jvm会优化为:现将引用地址赋值给INSTANCE变量后,再执行构造方法,如果两个线程t1,t2按如下时间序列执行:
时间1 t1 线程执行到INSTANCE = new Singleton(); 时间2 t1 线程分配空间,为Singleton对象生成了引用地址(0处) 时间3 t1线程将引用地址赋值给INSTANCE, 这时INSTANCE != null (7处) 时间4 t2 线程进入getInstance()方法,发现INSTANCE != null (synchronized块外) ,直接返回 INSTANCE 时间5 t1 线程执行Singleton的构造方法(4处)

这是t1还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么t2拿到将是一个未初始化完毕的单例
对INSTANCE使用volatile修饰即可,可以使用指令重排,但要注意在JDK5以上的版本的volatile才会真正有效

相关内容

热门资讯

大浪淘金APP黑幕揭秘:内盘操...   大浪淘金APP平台最隐蔽的黑幕,在于其完全封闭的内盘交易系统。这个看似与国际市场接轨的交易平台,...
告别固定工资实现财富自由?一品...   普通投资者被广告宣传诱导,注册就送现金,充值返现金券等等这些宣传方式,都是在引流到平台开户,不怕...
哈易购APP打着现货交易幌子搞...   网络上各类白银、铂金等贵金属投资广告铺天盖地,不少平台以"现货交易"为名,暗中却从事着非法期货交...
5元起投!高额收益?中国白银、...   上海华通白银国际交易中心有限公司(以下简称“华通白银国际”)打着“白银、铂金现货订购”的旗号,实...
"九州订购APP、起...   近年来,贵金属投资热度持续攀升,但乱象也随之而来。一些不法分子趁机搭建虚假交易平台,打着"低门槛...
坚锅淘金APP投资者频繁爆仓亏...   坚锅淘金APP对外宣称是贵金属现货交易平台,日常出入金看似畅通无阻,给人一种合规运营的假象。但事...
斗金订购APP“5元订购现货”...   斗金订购APP“5元订购现货”骗局揭秘:充值易、回本难,越陷越深!  许多投资人在未核实平台合规...
贵金属APP乱象曝光:&quo...   当前市场上部分贵金属类APP未取得正规交易资质,涉嫌误导消费者参与白银、铂金等商品的非合规交易!...
华通白银、银宝街APP被指虚假...   警惕现货白银投资陷阱:上海华通白银背后的平台涉嫌非法期货交易  近年来,各类现货白银交易平台以“...
充值送300,月入过万?别被量...   如今,网络上充斥着大量未经批准的贵金属及其他产品订购交易平台。这些平台普遍缺乏合法经营资质,却将...
鑫汇智投APP贵金属非法期货、...   鑫汇智投APP贵金属非法期货、人为控盘,大量投资者遭恶意收割  鑫汇智投APP长期以“低门槛、高...
矅光智策APP非法贵金属交易骗...   不少投资人在矅光智策APP上进行贵金属交易后出现了严重亏损。许多人在下载该平台时,根本不清楚其是...
飞鸽订购APP非法期货交易骗局...   飞鸽订购APP非法期货交易骗局,很多投资者根本没搞懂规则就盲目充值,买涨买跌跟买cai票一样!平...
天龙白银APP被操控的非法期货...   中小投资者在网络平台参与商品现货订购交易,表面看似合规合法,实则暗藏诸多难以察觉的风险。商品现货...
下载送试玩金?千万别碰!带你揭...   当前,许多贵金属APP交易平台正采用欺诈性营销手段。它们通过抖音等短视频平台,以“5元起投”“充...
泡泡淘金APP骗局:打着&qu...   泡泡淘金APP骗局:打着"商品订购"幌子,实则非法期货收割投资者!投资需谨慎,切勿轻信“高额回报...
南商云购APP虚假电子盘背后的...   南商云购是虚假电子盘吗?它是否具备合规的交易资质?这个曾经的"现货电子盘"如今摇身一变,披上了"...
警惕"祥龙订购&qu...   祥龙订购APP、掌上银牛APP等平台背后的运营方——四川禄宏商品现货市场有限公司,仅具备现货交易...
中国白银、华银:虚假宣传、后台...   中国白银APP宣称是上市公司“中国白银集团”与“上海华通白银国际交易中心”旗下的现货交易平台,提...
起底“优品订购”APP非法期货...   “优品订购”APP根本不具备任何合法的交易资质。其背后的运营方仅凭随便注册一家公司、开发一款交易...