Linking、Initializing
Linking分为三步:
- Verfication: 校验,校验是否符合class文件的标准
- Preparation:将class文件中的静态变量赋默认值
- Resolution:把class文件中常量池用到的符号引用。转化成内存地址,可以直接使用
将类、方法、属性等符号引用解析为指针、偏移量等内存地址的直接引用
常亮池中的各种符号引用解析为指针、偏移量等内存地址的直接引用
1 | package com.shifpeng.scaffold.service.jvm; |
如果把15、16行代码换个位置,那么输出的就是2
,拿交换后的代码分析:
1 | public class ClassLoaderProcedure { |
当我们调用T.class
的时候,首先将T.class
load进内存,接下来进行Verfication
进行校验,校验完进行Preparation
给静态变量赋默认值 ,那么此时
1 | t=null; |
接下来进行Resolution,这个不分析了,接下来进行initializing,给静态变量赋初始值 ,
此时
1 | t=new T(); |
还有一点非常类似的就是成员变量的赋值
1 | public class ClassLoaderProcedure { |
有一个成员变量,当new T()的时候,是一个申请内存的过程,申请完内存,他里面的成员变量先赋默认值0,然后开始调用构造方法,构造方法才会给赋初始值8。这跟Preparation、initializing有点像
静态变量实在类加载之后进行Preparation、initializing等。而对象中的成员变量是new出来之后才会进行这两步
单例模式&双重检查
有一道面试题:下面程序中的Mgr06
需不需要加volatile
?(DCL单例为什么要加volatile
)
答案:需要加,主要是指令重排的问题
1 | package com.shifpeng.scaffold.service.jvm; |
如果说不加volatile
会发生什么情况?
INSTANCE = new Mgr06();
在对这个INSTANCE
进行初始化的时候,如果初始化到一半的时候(什么叫初始化到一半?就是new Mgr06(),并且申请到了内存,里面的成员变量赋了默认值0;这个时候INSTANCE
已经指向到这个内存了,所以不再是空了),这个时候另外一个线程来了,首先判断INSTANCE == null
,发生不为空,所以就第二个线程就开始直接使用成员变量的默认值0了,而不是初始值了,这个时候就出问题了
解决这个问题的关键就是加volatile
那为什么加上这个volatile
就可以解决,这里是指令重排的问题
下面的代码,编译一下,查看字节码文件:
其中第一行,表示new T1,申请好内存空间
第三行:invokespecial调用构造方法,将成员变量属性a赋值为8
第4行:astore_1将上面的引用值赋值给t
正常情况下,应该是先调用完invokespecial
,再赋值给t,这样就不会出问题,由于指令有可能会重排,这两句指令发生的顺序有可能会不一样,即先把内存地址扔到t里了,然后在进行的初始化,就有可能发生有别的线程读到半初始化的现象
JMM Java内存模型(Java Memory Model)
1、高并发的情况下,Java内存内存模型是怎么提供支持的
2、一个对象在内存中是怎么布局的?
这里需要先说一下硬件层的并发优化基础知识,再将java的内存模型到底在硬件的基础上怎么实现的
硬件层的并发优化
存储器的层次结构
(深入理解计算机系统 原书第三版 P421)
离CPU越近,存储空间越小,但是速度越快,CPU要读数据的时候,假如一个数据是在硬盘上的(L5:),他首先是被load到内存,如果读数据的时候,CPU先高速缓存中去找(L1:),如果没有,继续去下一层(L2:),如果有,先load到最近的一层(L1:)(下次再去找的话就快了)
系统总线-总线锁
但是这里就会产生一个很严重的问题:
现在有两个CPU(或者两个核),假如有一个数在内存中(main memory),这个数会被load到L3 Cache中,L2和L1这两级缓存是在CPU的内部的,根据上面的图,我们就会想到,主存或者L3中的数据有可能会被load到不同的CPU内部,如果CPU1对修改X为1,CPU2修改X为2,那么就会存在数据不一致的情况。线程1拍跑在CPU1上,线程2跑在CPU2上,读到的数据是不一致的
那如何解决? 有许多种解决方式
多线程一致性的硬件层支持
通过系统总线 ,即L2通过一个系统总线
访问L3缓存数据(加把锁)
总线锁(bus lock
)会锁住总线,使得其他CPU甚至不能访问内存中其他的地址,因为效率较低
因此,总线锁 是老的CPU使用的
那新的CPU怎么解决的? 有各种各样的一致性协议 解决这个问题,而IntelCPU使用的是MESI,所以我们大多数聊的是MESI
因此,低层的一致性是怎么实现的? MESI
Cache Line 的概念、缓存行对其 伪共享
关于MESI
Cache一致性协议
有一篇文章可以看一下 【并发编程】MESI–CPU缓存一致性协议 可以参考学习一下,防止丢失,我截图出来:
通过MESI协议让来让各个CPU之间的缓存保持一致性
MESI称为缓存锁,缓存锁的一个实现就是MESI
有些无法被缓存的数据,活着跨越多个缓存行的数据,依然必须使用总线锁
现代CPU的数据一致性实现=缓存锁(MESI等)+ 总线锁
缓存行(面试)
从内存中读取数据放到CPU缓存中,读取缓存是以cache line为单位,目前为64byte
1、假如X、Y在一个缓存行,CPU1只用X,读取的时候,X、Y都会被读进来,CPU2只用Y,同样都会读进来
那么当CPU1把X做了修改,要通知其他CPU,通知其他CPU说“整个缓存行被改过了,这个缓存行已经是invalid
的状态了 ,麻烦你更新一下
”,那么CPU2就会把缓存行重新再读一遍。
2、同理,CPU2改完Y,通知一下,CPU1跟Y没有关系,但是还是重新读了一遍缓存行
两个互相无关的值因为变化,都需要重新读取一次缓存行
伪共享
位于同一缓存行的两个不同数据,被两个不同的CPU锁定,产生互相影响, 这种就叫伪共享
举个例子证明一下这个问题:
例子01:
1 | package com.shifpeng.scaffold.service.juc; |
例子02: 缓存行对齐
1 | package com.shifpeng.scaffold.service.juc; |
这个案例在开源软件disruptor
中也有用到高性能队列——Disruptor
综上,使用缓存行的对齐可以解决伪共享的问题,虽然会浪费一些空间,但是能够提升效率
乱序问题
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !