Java内存模型(JMM)
在高并发的情况下,java的内存模型到底是怎么提供支持的,要说清楚这个问题我们首先要先知道一些硬件层面的的知识,因为java的内存模型都是架构在这些硬件层面上的。计算机的存储结构
存储器的层次结构
计算机存储结构为金字塔型存储
这种结构主要由寄存器,缓存(cache),内存,硬盘,远程文件存储等这几部分组成。有如下两个特点:越接近金字塔顶端速度越快,容量越小,价格越贵 每一种存储器设备只和它相邻的存储设备打交道
需要注意的是L3高速缓存是主板上被所有CPU所共享的。
之所以是金字塔型的结构主要是局部性原理。
程序局部性原理时间局部性
刚被访问过的存储单元很可能不久又被访问,通常体现在循环执行的指令。让最近被访问过的信息保留在靠近CPU的存储器中加快处理速度。空间局部性
刚被访问过的存储单元的邻近单元很有可能不久会被访问,通常体现在顺序执行的指令。将刚被访问的存储单元的邻近单元调到靠近CPU的存储器加快访问。硬件层数据一致性
硬件层数据一致性
我们思考这样一个问题,当CPU需要主存中读取一个X数值时,最终会被CPU1和CPU2load到自己的核心中,如果CPU1修改了X的值,设置成了1,CPU2修改了X的值设置成了2,那么就产生了数据的不一致性,那么不同核中的数据要怎么保持一致性呢?也就是说我们运用了金字塔的存储模型,速度会提高。但是会有数据不一致性的问题。要解决这个问题就需要在硬件层面进行解决。方法如下:
总线锁
锁住总线
当CPU1通过总线bus访问x的值时,CPU2是不允许再访问x。因为锁住的是总线,除了访问x,访问其他的数值也是不允许的。
总线锁会锁住总线,使得其他CPU甚至不能访问内存中其他的地址,当时此种方式效率较低。
再老的一些CPU上会采用总线锁的方式。新的CPU采用各种各样的一致性协议。
一致性协议
缓存一致性协议
这里的一致性协议会有很多, 包括MSI、MESI、MOSI Synapse、Firefly及Draqon,intel采用的是MESI,我们这里重点介绍这种协议。
MESI(也称伊利诺斯协议)是一种广泛使用的支持写回策略的缓存一致性协议,该协议被应用在Intel奔腾系列的CPU中。
CPU中每个缓存行使用的4种状态进行标记(使用额外的两位bit表示)
状态
描述
M(Modified)修改
这行数据有效,数据被修改了,和内存中的数据不一样,数据只存在于本cache中。
E(Exclusive)独享/互斥
这行数据有效,数据和内存中的数据一致,数据只存下于本Cache中
S(Shared)共享
这行数据有效,数据和内存中的数据一致,数据存在于很多cache中
I(Invalid)无效
这行数据无效
E状态
E状态
只有Core 0访问变量x,它的Cache line状态为E(Exclusive)。
S状态
S状态
3个Core都访问变量x,它们对应的Cache line为S(Shared)状态。
M状态和状态之间的转化
M状态和状态之间的转化
Core 0修改了x的值之后,这个Cache line变成了M(Modified)状态,其他Core对应的Cache line变成了I(Invalid)状态。
在MESI协议中,每个Cache的Cache控制器不仅知道自己的读写操作,而且也监听其它Cache的读写操作。每个Cache line所处的状态根据本核和其它核的读写操作在4个状态间进行迁移。
需要注意的是MESI并没有完全解决锁总线的问题,我们说MESI是缓存锁,范围比起总线锁会小很多。但是有一些无法被缓存的数据或者跨越多个缓存行的数据还是需要总线锁。
现代CPU底层的数据一致性实现是采用总线锁加缓存锁来实现的。
缓存行
当CPU访问某个数据时,会假设该数据附近的数据以后会被访问到,因此,第一次访问这一块区域时,会将该数据连同附近区域的数据(共64字节)一起读取进缓存中,那么 这一块数据称为一个Cache Line 缓存行 。 在一般的x86环境下一个 CacheLine 是 64 字节。
伪共享
缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
伪共享
例如,CPU 1在读取数据X时,会把相邻的Y也会加载到其缓存行中,此时如果CPU2 需要读取数值Y,也会把相邻的X也加载缓存行中,此时如果CPU1修改数值X时根据缓存一致性协议会导致CPU2中的缓存行失效进而重读,如果CPU2修改数值Y时也会导致CPU1中的缓存行失效重读,这样就导致了CPU间彼此影响导致性能损耗。
一般我们通过缓存行对齐就可以解决这样的问题。
缓存行对齐
基于以上问题的分析,在一些情况下,比如会频繁进行操作的数据,可以根据缓存行的特性进行缓存行对齐(即将要操作的数据凑一个缓存行进行操作)下面使用一个示例进行说明: package com.example.demo; public class Cacheline_nopadding { public static class T{ //8字节 private volatile long x = 0L; } private static T[] arr = new T[2]; static { arr[0] = new T(); arr[1] = new T(); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for(long i = 0;i < 1000_0000L;i++){ //volatile的缓存一致性协议MESI或者锁总线,会消耗时间 arr[0].x = i; } }); Thread thread2 = new Thread(()->{ for(long i = 0;i< 1000_0000L;i++){ arr[1].x = i; } }); long startTime = System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000); } }
总计消耗时间:2508
下面来做一个改造升级,对齐缓存行,重点代码如下 private static class Padding{ //7*8字节 public volatile long p1,p2,p3,p4,p5,p6,p7; } public static class T extends Padding{ //8字节 private volatile long x = 0L; }
通过上述代码做缓存对齐,每次都会有初始的7*8个占位,加上最后一个就是独立的一块缓存行,整理后代码如下: package com.example.demo; public class Cacheline_padding { private static class Padding{ //7*8字节 public volatile long p1,p2,p3,p4,p5,p6,p7; } public static class T extends Padding{ //8字节 private volatile long x = 0L; } private static T[] arr = new T[2]; static { arr[0] = new T(); arr[1] = new T(); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(()->{ for(long i = 0;i < 1000_0000L;i++){ //volatile的缓存一致性协议MESI或者锁总线,会消耗时间 arr[0].x = i; } }); Thread thread2 = new Thread(()->{ for(long i = 0;i< 1000_0000L;i++){ arr[1].x = i; } }); long startTime = System.nanoTime(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("总计消耗时间:"+(System.nanoTime()-startTime)/100_000); } }
总计消耗时间:729
从上面可以看到,使用缓存对齐,相同操作情况下对齐后的时间比没对齐的时间减少一半。
上面这种缓存行填充的方法在早期是比较流行的一种解决办法,比较有名的Disruptor框架就采用了这种解决办法提高性能,Disruptor是一个线程内通信框架,用于线程里共享数据。与LinkedBlockingQueue类似,提供了一个高速的生产者消费者模型,广泛用于批量IO读写,在硬盘读写相关的程序中应用十分广泛,Apache旗下的HBase、Hive、Storm等框架都有使用Disruptor。硬件级别保证有序
乱序问题
CPU为了提高指令的执行效率,比如在执行一条读指令(读取内存的一条数据)时,去执行另一条指令,前提是两条指令间没有依赖关系。
我们来看这样的例子public class Disorder { private static int x = 0, y = 0; private static int a = 0, b =0; public static void main(String[] args) throws InterruptedException { int i = 0; for(;;) { i++; x = 0; y = 0; a = 0; b = 0; Thread one = new Thread(new Runnable() { public void run() { a = 1; x = b; } }); Thread other = new Thread(new Runnable() { public void run() { b = 1; y = a; } }); one.start();other.start(); one.join();other.join(); String result = "第" + i + "次 (" + x + "," + y + ")"; if(x == 0 && y == 0) { System.err.println(result); break; } else { //System.out.println(result); } } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); } }
在线程体中未发生乱序问题,那么程序输出的结果就不应该有x=0,y=0的结果,但是根据实际执行情况来看,会输出以下内容
第3932682次 (0,0)
也就是说,是存在乱序问题的。
硬件内存屏障 X86sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成
原子指令
如x86上的"lock …" 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。JVM级别保证有序性
JVM级别内存屏障
这些都是JVM级别定义的一些规范,具体的实现都是由硬件级别的内存屏障进行保障与支持的。volatile
volatile用来修饰成员变量(静态变量和实例变量),被修饰的变量在被修改时能够保证每个线程获取该变量的最新值,从而避免出现数据脏读的现象,也就是我们说的保证数据的可见性。volatile可以保证可见性和有序性,但是不能保证原子性。要保证原子需要synchronized和Lock。
字节码级别
字节码级别变化
被volatile修饰的变量在字节码级别的access flags中会多一个volatile字符串
JVM级别
JVM级别
在JVM级别就是根据volatile的写或读操作添加了相应的内存屏障。
StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;
StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序
LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序
LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序
硬件层面
通过相应工具查看汇编指令可以发现,在window上是通过lock指令来实现的。
因此,volatile修饰的变量具有以下的特点:Lock前缀的指令会引起处理器缓存写回内存;一个处理器的缓存回写到内存会导致其他处理器的缓存失效;当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
一个老人真正的幸福手里有钱,生活简素,精神富足01hr古人说人无再少,时无再来。古人又说停车坐爱枫林晚,霜叶红于二月花。时光一去不回,谁都没有办法阻止一个人变老。这样的惆怅,令人心头泛起了一阵忧伤。生活中,有的老人唉声叹气,有
进入冬月,老人喝酒忌讳多,牢记2喝2不喝,早知早受益不知不觉,天气越来越冷,眨眼就进了农历的11月,也就是冬月了。再往前数一个月,离过年也不远了,然而此时正是北方寒冬的季节,温度普遍下降得厉害。因此,不少老人喜欢喝酒御寒取暖,但出于
老人下肢水肿(水潴留),中医临床处方1首,健脾温肾,利湿消肿中医认为老人由于患慢性疾病较多,脏腑功能衰退,导致脾肾阳虚,水液输布功能失调,水湿潴留,而形成下肢水肿。治疗此症,根据脾肾虚的病理特点,以健脾温肾利湿消肿为法。方组黄芪太子参,山药
北京最容易偶遇明星的店要凉?夏雨杨坤投资黄渤曾站台资料图曾经风光一时的网红烘焙正处于落寞时刻。近日,有消费者反映牛角村储值卡使用受限,记者调查后发现该储值卡目前只能在直营店使用,而北京直营店仅剩两家。与此同时,其公司经营状态似乎也
苦并不能让人成长,只是成长过程中的副产品文老余小区被封了,今天没上班。早上社区里与人闲聊,当说到孩子教育问题时,分歧很大甲方觉得孩子就得吃苦,这能帮助孩子成长乙方觉得并不是这样的。你同意哪一方?要让你立即给出答案或许有点
宝宝为什么欢喜乱吃乱咬?2022育儿季其实这是宝宝能力发展的重要表现,通常从4个月左右开始。喜欢手吃吃脚拿起什么都往嘴里塞纸抽玩具服衣有时候连妈妈的脸蛋头发也不能幸免。出现样这的情况,就说明宝宝正于处以下
知道什么是侧切吗?多少宝妈的心声你们知道什么是侧切吗?就是在孩子不太容易岀来的时候用一把大剪子咔嚓咔嚓在女人的身上剪岀一个大口子让孩子顺利岀生我劝你们不要查这是史上十大恐怖视频之一女人是弱的而男人是强的无论是生理
最新研究孕妇可用针灸缓解疼痛据英国每日电讯报网站11月21日报道,根据一项新的全球数据分析结果,针灸可以显著缓解孕妇经常经历的下腰或骨盆疼痛。研究结果表明,孕妇选择用针灸缓解疼痛不会对胎儿产生明显不利的影响。
江苏无锡留住城市记忆,工业遗产活化成为文旅新地标高大古朴的白墙茂密树丛间的剧场松软草地上的天幕近日,位于马山的小田茶事工坊成为不少市民朋友圈里的网红休闲去处。很多人不知道,其实这是一处由废弃油库改造而成的景观,承载着旧院的回忆,
徒步苏州河畔长风湾烟囱广场,见证上海火柴工业历史时代的变迁烟囱广场最显著的建筑就是烟囱了上海试剂总厂大烟囱,高达62米,极具视觉冲击力。上海试剂总厂大烟囱是长风工业区保留下来的文物保护点,亲眼见证了百年苏州河的历史变迁。烟囱广场上有很多人
新疆旅行库车游记老城墙老巷老树老风光继续新疆旅行,本期目的地库车。到新疆旅行,就像到了一个伸手就能触到历史的地方,它们就摆在街道上在库车,那些代表着古老风光的遗迹往往近在咫尺。比如库车河东岸就有汉城墙遗址,314国道