一、volatile的基础含义
1.线程可见性
线程可见性即一个线程的修改对其他线程保持可见。
一般情况下,线程要访问一个变量,先把它的值复制到线程本地(即CPU寄存器中),另一个线程如果修改了该变量的值,前一个线程看不到。
volatile保证线程可见性是指,一个线程修改了马上写回内存,另一个线程下次读时,要从内存中重新读。
2.禁止指令重排序/禁止乱序执行
DCL单例是不是要加volatile的问题
系统底层如何保证有序性:①内存屏障sfence mefence lfence等系统原语②.锁总线
二、计算机CPU和缓存结构
计算机的组成
存储器的层次结构:
多核CPU一般有三级缓存:L1 L2 在核内,L3在核外多核共用。
补充: 超线程:一个ALU对应多个PC和Registers。如所谓的四核八线程。
三、volatile禁止指令重排序作用
DCL单例需要加volatile吗
1.单例
单例就是某一个类的对象在内存里头保证只有一个,这就是单例模式。
//饿汉模式单例
public class Test{
private static final Test INSTANCE = new Test();
private Test(){};
public static Test getInstance(){
return INSTANCE;
}
}
上述是最简单的一个单例的实现,构造方法为private只有自己能new,其他地方要使用这个对象,就调用getInstance方法,返回的永远都是这一个对象。
但是这是饿汉模式,如果需要懒汉模式,即用到的时候再初始化对象:
//懒汉模式单例
public class Test{
private static final Test INSTANCE;
private Test(){};
public static Test getInstance(){
if(INSTANCE == null){
INSTANCE = new Test();
return INSTANCE;
}
}
}
在调用getInstance()方法时new对象,先判断下这个对象是否为空,为空再new。
但是如上图这种getInstance()方法在多线程情况下不一定能保证只new了一个对象。解决这个问题可以给getInstance()方法加synchronized。
//懒汉模式单例方法加锁
public class Test{
private static final Test INSTANCE;
private Test(){};
public static synchronized Test getInstance(){
if(INSTANCE == null){
INSTANCE = new Test();
}
return INSTANCE;
}
}
但是锁住了整个方法,粒度太粗了,可以细化锁的粒度:
//懒汉模式单例代码块加锁
public class Test{
private static final Test INSTANCE;
private Test(){};
public static Test getInstance(){
//业务代码
//...
if (INSTANCE == null){
//试图通过减少同步代码块的方式提高效率,但还是不可行
synchronized (Test.class){
INSTANCE = new Test();
}
}
return INSTANCE;
}
}
这个代码也无法实现线程的安全性,还是会产生多个对象,所以诞生了下面一种写法:DCL
2.DCL——double check lock 单例
//懒汉模式单例DCL加锁
public class Test{
private static final Test INSTANCE;
private Test(){};
public static Test getInstance(){
//业务代码
//...
// Double Check Lock
if (INSTANCE == null){ //这里的if不能省略
synchronized (Test.class){
// 双重检查
if(INSTANCE == null){
INSTANCE = new Test();
}
}
}
return INSTANCE;
}
}
第一个if判空操作为什么不能省略?为了减少加锁时锁线程竞争的消耗。
3. DCL单例要不要加volatile?
一定要加
分析过程涉及到Java对象的创建过程
(1)Java对象创建源码和对应的汇编码
// 源码
class T{
int m = 8;
}
T t = new T();
//汇编码
0 new #2 <T>
3 dup
4 invokespecial #3 <T.<init>>
7 astore_1
8 return
(2)汇编码解析
0行 new 一个对象,相当于C中的malloc,按照对象的大小分配一块内存。此时,成员变量m的值为默认值0.
4行 invokespcecial特殊调用,调用的init即构造方法。此时m变为8.
7行 astore意思是在栈上的变量t和分配的堆内存之间建立关联。
即new一个对象时,有一个中间态,可以叫半初始化状态。此时刚刚分配内存空间还未调用构造方法,成员变量的值都为初始值0.
如果C/C++,为一个对象申请空间,成员变量值是上一个程序的遗留值。Java为对象申请一块内存,里面的成员变量赋初值,这也是Java在开始声称自己比C/C++安全的原因之一。
《effective Java》关于 Java编程的规则。其中一条规则:不要在构造方法里启动线程。可以在构造方法中把线程new好,在另外的方法中start启动。
(3)指令重排——半初始化状态
CPU可能会做些优化,4行和7行汇编指令会进行重排,如果在某个时刻,创建对象的指令执行完0行和7行的汇编码,此时另外一个线程访问该对象时,就会使用半初始化状态的对象,t指向了一块还未调用构造方法赋值的内存空间。
此时第二个线程运行,判断t不为空,直接拿去用,此时第二个线程使用了一个半初始化的对象。
(4)DCL单例
//懒汉模式单例DCL加锁
public class Test{
private static final /*volatile*/ Test INSTANCE;
private Test(){};
public static Test getInstance(){
//业务代码
//...
if (INSTANCE == null){ // Double Check Lock
synchronized (Test.class){
// 双重检查
if(INSTANCE == null){
INSTANCE = new Test();
}
}
}
return INSTANCE;
}
}
第二个线程thread 2不会进入第一个if (INSTANCE == null)的判断区域,因为此时t已赋值,不为空。
所以DCL单例 必须要加volatile.
加了volatile的作用,第一个作用:保持线程可见性,第二个作用:禁止指令重排序。
四、volatile禁止指令重排序的实现
1.volatile解决指令重排序,5个层面
源码层级:volatile i
字节码层级:加了一个标志ACC_VOLATILE
JVM层级 :要求对volatile的读写前后加屏障
hotspot的具体实现:锁总线
硬件层级:电信号
2.JVM的内存屏障
内存屏障就是一条栅栏,简单说,就是一堵墙。屏障的特点是屏障两边的指令不可以重排,保障有序。
在大多数系统底层,即CPU的层级上,都是有内存屏障的原语支持的,如lfence读屏障 sfence写屏障 mfence全屏障 等系统原语。
fence篱笆
LoadLoad屏障:如果屏障两侧有两条load指令,不能重排序
StoreStore屏障:如果屏障两侧有两条store指令,不能重排序
LoadStore屏障:如果屏障前后是Load和Store指令,不能重排序
StoreLoad屏障:如果屏障前后是Store和Load指令,不能重排序
load是读,store是写
以上屏障保证了volatile之前的读写操作对volatile之后的操作均可见。由JVM来加这个屏障。
3.hotspot具体实现
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
如果变量是volatile修饰的,就会加一个OrderAccess::fence()方法
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
用的是orderAccess的fence()方法,在这个方法中有一个lock addl指令,是lock了一个空操作(加0),往rsp寄存器上加了一个值0。也就是锁了总线。lock指令,锁总线。是一个Full Barrier,不仅解决了可见性问题,还解决了线程重排序问题。本质上跟synchronized一样,底层做一个同步,把所有的多线程改成单线程。
(nop空指令不能被lock,所以就lock了一个指令,往寄存器上加个0.)
因为上面说的sfence等原语有的CPU不支持。但是lock指令大多数CPU都支持。
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。
所有屏障都是这一条指令实现的:lock
汇编级的lock两种实现:锁定缓存行、锁总线
volatile不能保证原子性,就是因为这里只是在写回和读取volatile变量操作前后加屏障,对空操作锁总线,所以无法在读取-加1-写回整个操作中保持原子性,只能保证有序性和可见性。
五、cache line
1.概念
cache line 64字节,内存读入缓存的数据块的大小。
在CPU层级的数据一致性是以cache line为单位的。
一个CPU导致缓存行的内容改变,其他CPU要做同步。
2.disruptor关于缓存行的优化
disruptor单机最快的mq,本质是一个环形缓存 ring buffer。
(1)disruptor关于缓存行的优化的实现
有个指针cursor指出当前数据保存到了什么位置,指针会绕环走。要求指针支持多线程下快速访问。为了避免该指针与其他无关数据位于同一缓存行带来的效率降低,源码实现如下图。前后都加了7个long类型的数值。解决了伪共享的问题。
在常用的变量cursor前后加上7个long类型的变量,long类型8字节,一共56字节,说明cursor一定是自己一行,不会与别的数据在同一个缓存行。
这与JVM层面的volatile没有关系。
(2)disruptor关于缓存行的优化的原理
上面分析过,volatile实现可见性的方式是修改过的数据马上写回内存,并且其他CPU缓存中的数据也失效,下次读的时候要从内存读。CPU读内存数据到缓存的单位是缓存行。
在多线程情况下,如果需要修改“共享同一个缓存行的变量”,就会无意中影响彼此的性能,这就是伪共享(False Sharing)。
如果同一个缓存行中有其他数据a,更新cursor值的时候,在使用a的CPU缓存中,也需要把该缓存行内容标记为无效,使用时需要重新读取。所以通过前后填充无效变量的方式,让cursor自己占用一个缓存行,可以在一定程度提高效率。
Java8中新增了一个注解: @sun.misc.Contended 。加上这个注解的类会自动补齐缓存行,需要注意的是此注解默认是无效的,需要在jvm启动时设置 -XX:-RestrictContended 才会生效。
@sun.misc.Contended
public final static class VolatileLong {
public volatile long value = 0L;
//public long p1, p2, p3, p4, p5, p6;
}
(3)volatile和缓存一致性协议
标记了volatile之后CPU可以做到在缓存行之间保持数据一致性,这就是所谓的缓存一致性协议。
MESI是CPU核之间的数据一致性协议。因特尔的X86是MESI缓存的数据一致性协议。其他的CPU不一定是。
cache line有四种状态,modified被修改,exclusive独占(填充之后一个数据独占一个缓存行),shared共享读。invalid失效,如果有一个数据被修改了标记modified,另外CPU中标记失效invalid.
第一个CPU修改了把内容状态改为Modified,通知其他CPU把同一行改为Invalid。其他CPU访问时,如果是Invalid,就需要从内存再读一遍。
MESI是这四种状态的首字母
系统底层如何实现数据一致性:①MESI如果能解决,就使用MESI,②如果不能,就锁总线。
volatile和缓存一致性协议是两回事。
(4)既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?
a.volatile是java语言层面给出的保证,MSEI协议是多核cpu保证cache一致性的一种方法,是两个层次的概念。
b.MESI只是保证了多核cpu的独占cache之间的一致性,但是cpu的并不是直接把数据写入L1 cache的,中间还可能有store buffer。有些arm和power架构的cpu还可能有load buffer或者invalid queue等等。因此,有MESI协议远远不够。
c.consistency和coherence都可以被翻译为一致性,但是MSEI协议这里保证的仅仅coherence而不是consistency。那consistency和cohence有什么区别呢?
下面取自wiki的一段话: Coherence deals with maintaining a global order in which writes to a single location or single variable are seen by all processors. Consistency deals with the ordering of operations to multiple locations with respect to all processors.
因此,MESI协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。