Administrator
Published on 2021-06-30 / 184 Visits
0

【线程与并发】Volotile

Volotile

概念

意思为易变的; 无定性的; 无常性的; 可能急剧波动的; 不稳定的;等。
保证线程可见性。(底层靠CPU的缓存一致性协议MESI保证)
禁止指令重排序。(依靠内存屏障实现)
不能保证原子性,即不能替换synchronized。

线程可见性

java中存在堆内存,堆内存对所有线程共享,此外,每个线程都有自己的专属区域(工作内存),当一个线程访问一个值时,如下面的例子的running,会将此值复制到自己的工作空间里面,对此值的改变是先在自己的工作空间里面改变,会立刻写回共享空间,但是其他线程什么时候再读就不确定。即一个线程对某个值的改,并不会立刻反应到其他线程里,这就是线程之间不可见。

//测试保证线程可见性demo
public class Test {
    /*volatile */ boolean running = true;

    void m1(){
        System.out.println("开始调用");
        while (running){

        }
        System.out.println("结束调用");
    }
    public static void main(String[] args) {
        Test test = new Test();
        new Thread(test::m1).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        test.running = false;   //执行结果:m1()方法并不会退出;把volatile打开后就能退出,即主线程对running的修改,其他线程也立刻能知道,这就是保证线程可见性。

        //此句证实线程对一个值的改变是先在自己的工作空间里面改变,会立刻写回共享空间的。
        new Thread(() -> System.out.println(test.running)).start();
    }

    /* 输出结果:
    开始调用
    false
     */

    /* volatile打开后,输出结果:
    开始调用
    结束调用
    false
     */

}

禁止指令重排序

CPU层面上,现代的CPU为了效率,在执行指令时,并不是一条执行完再执行下一条,而是并发的执行,第一条没执行完,会直接执行第二条。 其也有重排序。
虚拟机层面上,编译器(类似java里的),编译完源码后,可能不一定按编写的代码顺序进行,而可能是根据优化规则,对指令进行重排序。
此外还有读写屏障的概念。

//懒汉式单例模型是否还需要加volatile?
public class Test {
    //本类内部创建对象实例
    //在双重检查机制下的懒汉式单例,是否还需要加volatile?
    private static /*volatile*/ Test instance = null;

    //构造方法私有化,外部不能new
    private Test() {

    }

    //提供一个公有的静态方法,返回实例对象
    public static Test getInstance() {
        if (instance == null) { //第一重检查,避免无效的synchronized 
            synchronized (Test.class){
                if (instance == null) { //第二重检查,单例必要的
                    instance = new Test();
                }
            }
        }
        return instance;
    }

}

//instance = new Test();编译器编译完后,它的指令分成三步:1.给对象申请内存,2.对对象的成员变量初始化,3.把这个块内存赋值给instance(instance在栈内存里)。
//如果出现指令重排序,3与2的执行顺序变换,那么在这种单例下不加volatile,就有可能出现,线程拿到的对象里面的成员变量是未初始化之前的默认值(如int就是0),这种情况可能出现在超高超高并发下。

DCL单例指的是具有双重检查的单例模式:
https://www.cnblogs.com/codingmengmeng/p/9846131.html

volatile如何解决指令重排

1.代码层,关键字volatile
2.字节码层,ACC_VOLATILE
3.JVM的内存屏障(遵循JSR)
StoreStoreBarrier -> vloatile 写操作 -> StoreLoadBarrier
LoadLoadBarrier -> volatile 读操作 -> LoadStoreBarrier
4.hostspot实现
bytecodeinterpreter.cpp -> orderAccess fence() -> lock addl

CPU是有sfence mfence lfence等系统原语支持内存屏障的(有些CPU不支持),只是hostspoty用的是lock(锁总线)

内存屏障

屏障两边的指令不能重排,保证有序。

JSR内存屏障
LoadLoad,StoreStore,LoadStore,StoreLoad四种
其中Load代表读,Store代表写。
image.png