Java 多线程 : 漫谈 Volatile-灵析社区

带鱼

一 .  volatile 基础

> volatile 保证内存的可见性 并且 禁止指令重排
> volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的  
> 保证线程可见性且提供了一定的有序性


    
// Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。

二 .  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 的区别

  1. volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取。synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile 仅能使用在变量级别。synchronized 则可以使用在变量、方法、和类级别的。
  3. volatile 仅能实现变量的修改可见性,不能保证原子性。而synchronized 则可以保证变量的修改可见性和原子性。
  4. volatile 不会造成线程的阻塞。synchronized 可能会造成线程的阻塞。
  5. volatile 标记的变量不会被编译器优化。synchronized标记的变量可以被编译器优化。
注意 :volatile 不能取代 synchronized

四 . volatile  原理

观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令,其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile 的底层就是通过内存屏障来实现的
  • Step 1 : 写volatile的时候生成汇编码是 lock addl $0x0, (%rsp)
  • Step 2 : 在写操作之前使用了lock前缀,锁住了总线和对应的地址,这样其他的写和读都要等待锁的释放。
  • Step 3 : 当写完成后,释放锁,把缓存刷新到主内存。
  1. 读volatile就很好理解了,不需要额外的汇编指令,CPU发现对应地址的缓存被锁了,等待锁的释放,缓存一致性协议会保证它读到最新的值。
  2. 只需要对写volatile的使用用lock对总线加锁就行了,这样其他的读、写操作等待总线释放才能继续读。Lock会让其他CPU的缓存invalide,从内存重新加载数据。
// 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   原子性

> 我们需要区别 volatile 变量和 atomic 变量
// volatile 并不能很好的保证原子性
volatile 变量,可以确保先行关系,即写操作会发生在后续的读操作之前,但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
AtomicInteger 类提供的 atomic 方法,可以让这种操作具有原子性。例如 #getAndIncrement() 方法,会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

六 . volatile 源码

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 的作用是 : 仲裁所有的内存访问操作
	
        

七 . volatile 实测

// 测试原子性 , 结果 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