JMM概念
【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证。
三大特性:原子性,可见性,有序性。
作用:缓存一致性协议,用于定义数据读写的规则;
硬件层数据一致性
硬件层缓存数据一致性协议很多;其中intel用MESI
现代CPU的数据一致性实现为缓存锁(MESI协议)与总线锁
MESI Cache一致性协议如图
读取缓存以cache line 为基本单位,目前64bytes
使用缓存行对齐可以提高效率
伪共享:位于同一缓存行的两个不同数据,被两个不同cpu锁定,如果其中之一修改从而产生相互影响的
参考:https://www.cnblogs.com/z00377750/p/9180644.html
缓存行代码验证
public class CacheLinePadding1 {
private static class T {
public volatile long x = 0L;// 8个字节
}
public static T[] arr =new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread (()->{
for (long i = 0;i<10000_0000L;i++){
arr[0].x = i;
}
});
Thread t2 = new Thread (()->{
for (long i = 0;i<10000_0000L;i++){
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/10000_0000L);
}
}
打印显示
public class CacheLinePadding2 {
private static class Padding{
public volatile long p1 ,p2 ,p3,p4,p5,p6,p7;
}
//缓存行对齐
// 1 byte = 8 bit
// long占 8byte(64bit位)
// 加上x刚好 8*8byte = 64bytes
private static class T extends Padding{
public volatile long x = 0l;
}
public static T[] arr =new T[2];
static {
arr[0] = new T();
arr[1] = new T();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread (()->{
for (long i = 0;i<10000_0000L;i++){
arr[0].x = i;
}
});
Thread t2 = new Thread (()->{
for (long i = 0;i<10000_0000L;i++){
arr[1].x = i;
}
});
final long start = System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()-start)/10000_0000L);
}
}
信息打印
软件层
乱序问题
JMM定义了线程工作内容与主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory)
CPU为了提高指令执行效率,会在一条指令执行过程中(如去内存读取数据比CPU读(慢100倍)),去同时执行另一条指令,前提是,两条指令之间没有依赖关系;
参考:https://www.cnblogs.com/liushaodong/_p/4777308.html
写操作也可以进行合并
乱序证明
/**
* 乱序证明
*/
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;
CountDownLatch down = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
a = 1;
x = b;
down.countDown();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
down.countDown();
}
});
t1.start();
t2.start();
t2.join();
t2.join();
down.await();
String result = "第"+i+"次("+x+","+y+")";
if(x==0 && y==0){
System.err.println(result);
break;
}
}
}
}
有序性保证
参考:https://blog.csdn.net/weixin_45189427/article/details/120310698
CPU内存屏障
内存屏障是特殊指令:看到这种指令,前面的必须执行完成,后面才执行
intel:lfence sfence mfence(CPU特有指令)
X86CPU内存屏障
- sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成
- lfence:在lfence指令钱的都操作当必须在lfence指令后的读操作前完成
- mfence:在mfence指令钱的读写操作当必须在mfence指令后的读写操作前完成
intel lock汇编指令
原子指令,如x86上的lock指令是一个fullbarrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU.software locks 通常使用了内存屏障或者原子指令来实现变量可见性和保持程序顺序
JVM中的内存屏障
所有实现JVM规范的虚拟机,必须实现四个屏障
- LoadLoad屏障:
对于这样的语句Load1;LoadLoad;Load2
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。 - StoreStroe屏障:
对于这样的语句Store1;StoreStore;Store2
在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见。 - LoadStroe屏障:
对于这样的语句Load1;LoadStore;Store2
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。 - StroeLoad屏障:
对于这样的语句Store1;StoreLoad;Load2
在Load2及后续所有读取操作执行前,保证Store1的写入操作对其他处理器可见。
final语义中内存屏障
对于final域,编译器和CPU会遵循两个排序规则:
新建对象过程中,构造体中final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序
初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(先赋值引用,再调用final值)
上述可理解为:必须保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障起的作用
写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程CPU可见,并阻止重排序。
读final域,在上述规则2中,两步操作不能重复排序的机理就是在读final域前插入了LoadLoad屏障
X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略
volatile的底层实现
- 可见性
- 禁止指令重拍
volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序
基于jvm的内存屏障
# 写
---StoreStroeBarrier---
# volatile 写代码
---StoreLoadBarrier---
#读
# volatile 读代码
---LoadLoadBarrier---
---LoadStoreBarrier---
hotspot实现
volatile关键字
解决共享对象可见性问题:volilate
两点作用:
1.保证线程间变量的可见性。
2.禁止CPU进行指令重排序。
可见性
volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
volatile保证可见性的流程大概就是这个一个过程:
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。