> volatile 保证内存的可见性 并且 禁止指令重排
> volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的
> 保证线程可见性且提供了一定的有序性
// Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。
> 读写主存中的数据没有 CPU 中执行指令的速度快 , 为了提高效率 , 使用 CPU 高速缓存来提高效率
> CPU 高速缓存 : CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关
// 原理 @ https://www.cnblogs.com/xrq730/p/7048693.html
Step 1 : 先说说 CPU 缓存 , CPU 有多级缓存 , 查询数据会由一级到三级中
一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存
二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半
三级缓存:简称L3 Cache,部分高端CPU才有
// 缓存的加载次序
1 > 程序以及数据被加载到主内存
2 > 指令和数据被加载到CPU缓存
3 > CPU执行指令,把结果写到高速缓存
4 > 高速缓存中的数据写回主内存
// Step End : 因为不同的缓存 , 就出现了数据不一致 , 所以出现了规则
当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效
注意 :volatile 不能取代 synchronized
观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令,其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile 的底层就是通过内存屏障来实现的
// volatile 的内存语义
- 当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值,立即刷新到主内存中。
- 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
> 所以 volatile 的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
// volatile 的内存语义实现原理 : 为了实现 volatile 的内存语义,JMM 会限制重排序
1. 如果第一个操作为 volatile 读,则不管第二个操作是啥,都不能重排序。
?- 这个操作确保volatile 读之后的操作,不会被编译器重排序到 volatile 读之前;
2. 如果第二个操作为 volatile 写,则不管第一个操作是啥,都不能重排序。
?- 这个操作确保volatile 写之前的操作,不会被编译器重排序到 volatile 写之后;
3. 当第一个操作 volatile 写,第二个操作为 volatile 读时,不能重排序。
// volatile 的底层实现 : 内存屏障 , 有了内存屏障, 就可以避免重排序
-> 对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM 采用了保守策略
• 在每一个 volatile 写操作前面,插入一个 StoreStore 屏障
- StoreStore 屏障:保证在 volatile 写之前,其前面的所有普通写操作,都已经刷新到主内存中。
• 在每一个 volatile 写操作后面,插入一个 StoreLoad 屏障
- StoreLoad 屏障:避免 volatile 写,与后面可能有的 volatile 读 / 写操作重排序。
• 在每一个 volatile 读操作后面,插入一个 LoadLoad 屏障
- LoadLoad 屏障:禁止处理器把上面的 volatile读,与下面的普通读重排序。
• 在每一个 volatile 读操作后面,插入一个 LoadStore 屏障
- LoadStore 屏障:禁止处理器把上面的 volatile读,与下面的普通写重排序。
> 我们需要区别 volatile 变量和 atomic 变量
// volatile 并不能很好的保证原子性
volatile 变量,可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
AtomicInteger 类提供的 atomic 方法,可以让这种操作具有原子性。例如 #getAndIncrement() 方法,会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
TODO : 涉及到源码 ,先留坑 , 具体可以先看 @ https://www.cnblogs.com/xrq730/p/7048693.html
// 主要节点 :
0x0000000002931351: lock add dword ptr [rsp],0h ;
*putstatic instance;
- org.xrq.test.design.singleton.LazySingleton::getInstance@13 (line 14)
> 将双字节的栈指针寄存器+0 , 保证volatile关键字的内存可见性
// 基本概念一 : LOCK# 的作用
- 锁总线
- 其它CPU对内存的读写请求都会被阻塞,直到锁释放
- 不过实际后来的处理器都采用锁缓存替代锁总线
- 因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
- lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
- 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
// 基本概念二 : 缓存行
- 缓存是分段(line)的,一个段对应一块存储空间 , 即缓存行
- CPU看到一条读取内存的指令时,它会把内存地址传递给一级数据缓存
- 一级数据缓存检测是否由缓存段 , 没有加载这缓存段
// 原因 : volatile 基于 缓存一致性来实现
Step1 : 因为LOCK 效率问题 ,所以基于缓存一致性来处理
Step2 : 缓存一致性作用时 使用多组缓存,但是它们的行为看起来只有一组缓存那样
Step3 : 常见的协议是 snooping 和 MESI
Step4 : snooping 的作用是 : 仲裁所有的内存访问操作
// 测试原子性 , 结果 ThreadC : ------> count :9823 <-------
// Thread 中操作
public static void addCount() {
for (int i = 0; i < 100; i++) {
count++;
}
logger.info("------> count :{} <-------", count);
}
ThreadC[] threadCS = new ThreadC[100];
for (int i = 0; i < 100; i++) {
threadCS[i] = new ThreadC();
}
for (int i = 0; i < 100; i++) {
threadCS[i].start();
}
// 添加 synchronized 后 -- > ThreadD count :10000 <-------
synchronized public static void addCount()
阅读量:2015
点赞量:0
收藏量:0