JMM
先看单例模式上最著名的DCL(double check loading)问题。
public class Singleton { private static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){ if (singleton == null){ synchronized(Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; }}
|
如果变量不加voliate会出什么问题?
假设此时有两个线程thread1和thread2在进行操作。
此时thread执行到new Singleton(),而thread2执行到singleton的判空操作,此时由于synchronized具有指令重排,不具有原子性,将会导致singleton初始化后不完整的问题,最后产生NPE异常。接下来解释为什么会产生初始化不完整问题。
singleton = new Singleton();这行代码到底做了什么事情,大致过程如下:
① 虚拟机遇到new指令,到常量池定位到这个类的符号引用。
② 检查符号引用代表的类是否被加载、解析、初始化过。
③ 虚拟机为对象分配内存。
④ 虚拟机将分配到的内存空间都初始化为零值。
⑤ 虚拟机对对象进行必要的设置。
⑥ 执行方法,成员变量进行初始化。
⑦ 将对象的引用指向这个内存区域。
我们把这个过程简化一下,简化成3个步骤:
a、JVM为对象分配一块内存M
b、在内存M上为对象进行初始化
c、将内存M的地址复制给singleton变量
因为将内存的地址赋值给singleton变量是最后一步,所以Thread1在这一步骤执行之前,Thread2在对singleton==null进行判断一直都是true的,那么他会一直阻塞,直到Thread1将这一步骤执行完。
但是,以上过程并不是一个原子操作,并且编译器可能会进行重排序,如果以上步骤被重排成:
a、JVM为对象分配一块内存M
c、将内存的地址复制给singleton变量
b、在内存M上为对象进行初始化
这样的话,Thread1会先执行内存分配,在执行变量赋值,最后执行对象的初始化,那么,也就是说,在Thread1还没有为对象进行初始化的时候,Thread2进来判断singleton==null就可能提前得到一个false,则会返回一个不完整的sigleton对象,因为他还未完成初始化操作。
而voliate能够避免指令重排
1 public class Singleton { 2 private volatile static Singleton singleton; 3 private Singleton (){} 4 public static Singleton getSingleton() { 5 if (singleton == null) { 6 synchronized (Singleton.class) { 7 if (singleton == null) { 8 singleton = new Singleton(); 9 } 10 } 11 } 12 return singleton; 13 } 14 } |
而上述内容牵扯到jmm,接下来就是对jmm内容的讲解。
硬件层的并发优化基础知识
线程1、2公共使用同一个CacheLine
x、y在同一个CacheLine
x、y都是volatile
如果线程1不断修改x,线程2不断修改y,那么修改的时候线程1就要不断通知线程2更新x、线程2就要不断通知线程1更新y
这样的不断通知不断重新读取很浪费性能
这就叫伪共享
Cache Line 是 CPU 和主存之间数据传输的最小单位。当一行 Cache Line 被从内存拷贝到 Cache 里,Cache 里会为这个 Cache Line 创建一个条目。
弄懂伪共享问题的前提是:cpu如果想读取x的信息时,会把一整个缓存行的信息读取到cpu的缓存行中,即把x和y的数据一起读入。
伪共享问题:在左边上运行的线程想更新L3中缓存变量X,同时右边上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果左边的进程获得了所有权,缓存子系统将会使右边中对应的缓存行失效。此时会通知右边的cpu把x的值进行更改,右边的cpu则去L3中获取重新。当右边获得了所有权然后执行更新操作,左边就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。(并发的修改在一个缓存行中的多个独立变量,看起来是并发执行的,但实际在CPU处理的时候,是串行执行的,并发的性能大打折扣。)
解决办法:使用各种各样的一致性协议(例如MESI协议)
在Java中提供的办法:填充法 和 Contended 注解
填充法的原理如下:
多线程会有上面的伪共享的问题,如果在缓存读取数据到CacheLine时,两个volatile的数被读取到不同的CacheLine中的话,就不需要一直通知另一个线程更新数据了,因为另一个线程根本没有这个数据
那么如何让两个数据一定在不同的CacheLine呢,方法就是Cache Line对齐
一般一个CacheLine是64位,也就是8个long,我们可以把x定义为long,并同时定义7个没有用的long变量,这样这8个数就在同一个CacheLine中
之后再定义y,y自然也就在下一个CacheLine中了
乱序问题
CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系(注解:没有依赖关系,指两条指令没有任何关系,例如 int a=8;a++;此时两条指令存在依赖关系。Int a=8 int b=9,两条指令就是没有任何依赖关系)
https://www.cnblogs.com/liushaodong/p/4777308.html
写操作也可以进行合并
https://www.cnblogs.com/liushaodong/p/4777308.html
当cpu执行存储指令时,它会首先试图将数据写到离cpu最近的L1_cache, 如果此时cpu出现L1未命中,则会访问下一级缓存。速度上L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍,而且还存在L2_cache不命中的情况,又需要更多的周期去主存读取。其实在L1_cache未命中以后,cpu就会使用一个另外的缓冲区,叫做合并写存储缓冲区。这一技术称为合并写入技术。在请求L2_cache缓存行的所有权尚未完成时,cpu会把待写入的数据写入到合并写存储缓冲区,该缓冲区大小和一个cache line大小,一般都是64字节。这个缓冲区允许cpu在写入或者读取该缓冲区数据的同时继续执行其他指令,这就缓解了cpu写数据时cache miss时的性能影响。
JUC/029_WriteCombining
乱序执行的证明:JVM/jmm/Disorder.java
原始参考:https://preshing.com/20120515/memory-reordering-caught-in-the-act/
乱序的证明
package jmm;
|
如何保证特定情况下不乱序?
硬件层面的不乱序:
硬件内存屏障 X86
sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序
JVM级别如何规范(JSR133)
LoadLoad屏障:
对于这样的语句Load1; LoadLoad; Load2,
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:
对于这样的语句Store1; StoreStore; Store2,
在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:
对于这样的语句Load1; LoadStore; Store2,
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:
对于这样的语句Store1; StoreLoad; Load2,
在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
volatile的实现细节
字节码层面
ACC_VOLATILE
JVM层面
volatile内存区的读写 都加屏障
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
OS和硬件层面
https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis - HotSpot Dis Assembler
windows lock 指令实现 | MESI实现
synchronized实现细节
字节码层面
ACC_SYNCHRONIZED
monitorenter monitorexit
JVM层面
C C++ 调用了操作系统提供的同步机制
OS和硬件层面
X86 : lock cmpxchg / xxx
https88571740