| |

VerySource

 Forgot password?
 Register
Search
View: 1167|Reply: 0

volatile关键字

[Copy link]

1

Threads

1

Posts

2.00

Credits

Newbie

Rank: 1

Credits
2.00

 China

Post time: 2021-11-5 11:39:17
| Show all posts |Read mode

前言昨天跟小万探讨了一下可见性到底是什么问题导致的,自己的理解还是有问题,吓得我赶紧回去复习了一下。
volatile作用1、保证线程间的可见性;
2、防止指令重排序。
可见性在OS中如何实现要从操作系统底层理解volatile的可见性如何实现,需要复习一下计算机组成原理的知识。
计算机组成原理如下图:


           
  • PC 程序计算器:用于存放指令
           
  • Registers 寄存器:用于存放从内存中获取的数据
           
  • ALU 算数计算器:用于计算
           
  • cache:每个cpu都拥有自己的两个高速缓存区L1和L2,多个cpu共享L3缓存区和内存
多核CPU如下图所示,一个单cpu两核的服务器。由于在cpu中的速度比在内存中的速度快太多了,所以cpu内部是存在多级高速缓存的。LI和L2是每个核独有的高速缓存,L3多核共享。cpu首先会从Registers 寄存器获取数据,如果没取到,会从L1取,如果再取不到从L2取,以此类推,缓存中找不到,会直接从内存中取。
那么问题来了(超线程除外),假设有两个线程都获取了a=1的值,并放到了高速缓存中,线程1把a=1改为a=2并更新到了内存;此时线程1(核1)让出cpu,线程2(核2)进来,如果线程1的操作对于线程2是不可见的,那么线程2获取到a的值将仍然为1。所谓可见性,就是线程1把a的值改了,得通知线程2你要重新从内存中获取最新的值,不能从缓存中取了。在操作系统层面会使用MESI解决缓存层面的可见性问题,但是在jvm层面会使用volatile关键字解决可见性问题。

MESI(Modify Exclusive Shared Invalid)当CPU1中的x被修改后该缓存行会被标记为Modify,然后通过总线通知持有该缓存行的cpu2你的缓存行已经Invalid了,需要从内存中重新读取,cpu1把modify后的数据写回内存 。
volatile的另外一个用法,如下:
public class Test{    public long t1,t2,t3,t4,t5,t6,t7;        public volatile long t = 0L;    public long t8,t9,t10,t11,t12,t13,t14;    }
为何要定义多个long类型的变量呢?
根据缓存一致性协议,当一个cache line 被修改了之后,会通知其它用到该cache line的线程需要更新了。存在一种情况,在同一个缓存行里面,有一个线程修改x,另外一个线程修改y,它们都在修改的时候,都需要到内存中取最新的数据,这就导致效率变低了。
那如何能提高效率呢?空间换时间。如上面代码,一个long类型占8个字节,t(相当于x和y)的前后都有七个long类型,加上t刚好64个字节,而一个cache line的字节数也是64个字节,这就保证了x和y不在同一个缓存行里面,它们的修改互不影响,从而提高了效率。
超线程常常会听说4核8线程不知道具体什么意思,学习之后发现,其实就是一个核可以同时装两个线程,一个ALU对应多组PC|Registers,在切换线程的时候,不需要再从其它地方取回原来线程的数据,只需要ALU挪到对应的PC|Registers组就可以了,线程切换的速度会快很多。
指令重排序当两行代码没有关联,如a=1,b=1,操作系统就有可能先执行b=1,再执行a=1。
案例:DCL需不需加volatile关键字?
DCL(double check lock)/** * @author kobe * @date 2021-02-05 19:30 */public class Test {    //需不需要加volatile关键字修饰.    private static Test INSTANCT = null;    private Test(){};    public static Test getInstance(){        //第一次        if (INSTANCT == null){            //第二次            synchronized (Test.class){                if (INSTANCT == null){                    INSTANCT = new Test();                }            }        }        return INSTANCT;    }}
分析这个问题需要了解对象的创建过程。
对象创建过程public class Test {    public static void main(String[] args) {        Object o = new Object();    }}
new Object()在操作系统的汇编码中究竟做了什么?

           
  • new 一块内存空间,并初始化变量值为0(半初始化)
           
  • dup
           
  • invokespecial <init> 变量赋值
           
  • astore_1 关联对象
           
  • return
上面的汇编码可以通过IDEA的一个插件获取:jclasslib
回到上面DCL的问题,当第一个线程做完了new的动作,此时invokespecial与astore_1的指令调换了顺序,此时第二个线程进来判断INSTANCT 是否为空,因为第一个线程在new的时候已经初始化了对象,所以不为空,此时第二个线程获取的对象将会是一个半初始化的对象。
或许有疑问,为什么是个半初始化的对象?
假设一个对象有变量a=1000,在汇编码中的第一步new动作只会初始化a的值等于0,当执行到invokespecial的时候才会把1000赋值给a,然后再关联对象,因为发生了指令重排序,导致初始化后直接关联了对象,所以就会成为一个半初始化的对象。
所以,DCL必须加volatile关键字。
OS如何防止指令重排序?OS使用了内存屏障的技术来防止指令重排序。
屏障两边的指令不可以重排!保障有序

           
  • LoadLoad屏障
           
  • StoreStore屏障
           
  • LoadStore屏障
           
  • StoreLoad屏障

Reply

Use magic Report

You have to log in before you can reply Login | Register

Points Rules

Contact us|Archive|Mobile|CopyRight © 2008-2023|verysource.com ( 京ICP备17048824号-1 )

Quick Reply To Top Return to the list