volatile相关

一、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协议最多只是保证了对于一个变量,在多个核上的读写顺序,对于多个变量而言是没有任何保证的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值