并发编程之volatile

一、Java内存模型内存交互操作

1、lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态

2、unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

3、read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

4、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

5、use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎

6、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量

7、store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作

8、write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

整个执行流程如图

 

 read ---load  store----writr必须成对执行

通过上面分析我们可以看出即使在java里面执行i++这样的操作,对于我们的底层来说也不是原子操作,因为i++,也需要将这八大操作走一遍,具体来说,read ---load 将主内存中i=0在工作内存中也copy一份,

线程读到工作内存中的i=0并加1操作即结果i=1写回工作内存(use---assign),然后将i=1写回主内存(store----writrt)这一步如果没有用缓存一致性协议,会有延时不会立即写到主内存,参考第一篇缓存一执行性协议讲解。

二、volatile原理与内存语义

volatile是Java虚拟机提供的轻量级的同步机制

volatile语义有如下两个作用

可见性:保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。

有序性:禁止指令重排序优化。

volatile缓存可见性实现原理

JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步会主内存,使用时必须从主内存刷新,由此保证volatile变量的可见性。 底层实现:通过汇编lock前缀指令,它会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存内存会导致其他处理器的缓存无效

三、volatile可见性分析

先上一段代码:

public class VolatileVisibilitySample {
    private boolean  initFlag = false;
    static Object object = new Object();

    public void refresh(){
        this.initFlag = true; //普通写操作,(volatile写)
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }

    public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
    }

    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");

        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");

        threadB.start();
        try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }

}

代码很好理解,线程B读取成员变量initFlag 如果为false无线循环,如果为true,打出表示语,线程A负责将initFlag改为true,线程B先启动,线程A启动修改标志为true后,看看线程B能否感知到并终止循环

测试结果 :线程B无线循环,未能感知到标志被线程A修改,原因,线程B一直读的是工作空间的缓存数据,当线程A修改数据之后,线程B未能感知到.

降上诉代码修改,线程B的执行任务上加锁synchronized:

 public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
            synchronized (object){
                i++;
            }
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
    }

测试结果:加锁会导致线程B失去cpu执行权,当再次获取cpu执行权时,会引起线程上下文切换,这个过程会引起重新读取主内存数据。

volatile关键字测试

initFlag用volatile修饰后

 private volatile boolean  initFlag = false;

测试结果:当线程A修改initFlag后线程B能立即感知到,停止循环打出标志语;

原因:线程A修改initFlag,由于initFlag被volatile修饰,会立即从工作内存刷到主内存,同时让其他线程中工作内存中initFlag数据缓存失效,这样线程B中原来地缓存失效,从主内存中重新读取新值。

四、volatile不能保证原子性

先来一段代码:

public class VolatileAtomicSample {

    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter++; //不是一个原子操作,第一轮循环结果是没有刷入主存,这一轮循环已经无效
                    //1 load counter 到工作内存
                    //2 add counter 执行自加
                    //其他的代码段?
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter);
    }

}

开10个线程每个线程对counter进行1000次+最总我们地结果也不是10000,而是小于10000,

原因:couter++并不是原子操作,比如两个线程读到counter=0都读到自己地工作内存,然后加1之后都要往我们地主内存写,这时候必然引起裁决,导致一个线程的+1有效果,一个线程的+1无效果,最后导致

两个线程一共加了两次1,只有一个有效,最后结果比预期结果小。

五、volatile保证有序性防止指令重排

有序性问题

 在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述volatile关键字)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

指令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能
 
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。也就是说指令重排只能保证单线程没有问题,不能保证多线程安全。
 

指令从排序发生在编译重排序和处理器重排序,禁止指令重排序的底层就是内存屏障,内存屏障分为4种

1、StoreStore  2、StoreLoad  3、LoadLoad  4、LoadStore

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
(1)∙在每个volatile写操作的前面插入一个StoreStore屏障。
(2)在每个volatile写操作的后面插入一个StoreLoad屏障。
(3)在每个volatile读操作的后面插入一个LoadLoad屏障。
(4)在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
 
小知识点,不用volatile如何防止指令重排:
手动加内存屏障
 public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                    shortWait(10000);
                    a = 1; //是读还是写?store,volatile写
                    //storeload ,读写屏障,不允许volatile写与第二部volatile读发生重排
                    //手动加内存屏障
                    UnsafeInstance.reflectGetUnsafe().storeFence();
                    x = b; // 读还是写?读写都有,先读volatile,写普通变量
                    //分两步进行,第一步先volatile读,第二步再普通写
                }
            });

六、总线风暴问题

大量使用volatile会引起工作缓存有大量的无效缓存,而且volatile会一起会引起线程之间相互监听,嗅探,这些都会占用总线资源,导致总线资源负载过高。这时候我们需要锁来解决问题,这就是为什么有了

volatile我们还需要synchronized,lock锁,因为volatile保证不了原子操作,且用的过多会导致总线风暴。

七、volatile,synchronized同时使用-----一个超高并发的单例场景

public class Singleton {

    /**
     * 查看汇编指令
     * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
     */
    private volatile static Singleton myinstance;

    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();//对象创建过程,本质可以分文三步
                    //对象延迟初始化
                    //
                }
            }
        }
        return myinstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

解释:创建对象myinstance = new Singleton() 并不时一个原子操作,它可以分为三部,1、申请空间,2,实力化对象,3,地址赋值给myinstance 变量,加synchronized 保证了原子操作,但是无法防止指令重排,线程1申请完空间之后如果发生指令重排直接执行第3步赋值,那么线程2执行if判断时myinstance 不为空但是却没有实例化对象。这是指令重排导致的,所以volatile 修饰myinstance防止发生指令重排。----超高并发下的应用。

posted @ 2020-07-11 19:35  菜鸟的java世界  阅读(588)  评论(0编辑  收藏  举报