volatile

内存可见性

先上一个示例:

package com.fen.dou.stu.fenbushisuo;

import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
  //  static   ConcurrentHashMap map = new ConcurrentHashMap();
    static   HashMap map = new HashMap();
   // static volatile  HashMap map = new HashMap();
    public static void main(String[] args) {
        
        new Thread(()->{
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put("a","a");
        },"A").start();


        new Thread(()->{
            while (true){
                String ssss = (String) map.get("a");
                if(ssss != null){
                    System.out.println("-----------------------"+Thread.currentThread().getName());
                    break;
                }

            }
        },"B").start();


        new Thread(()->{

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while (true){
                String ssss = (String) map.get("a");
                if(ssss != null){
                    System.out.println("-----------------------"+Thread.currentThread().getName());
                    break;
                }

            }
        },"C").start();
    }
}

这个代码最终会打印-----------------------C

有人会感觉疑惑,线程C执行的就比线程B执行多了一个Thread.sleep(3000);,为什么线程C会打印? 这是因为JMM中有Happens-Before规则,看代码

线程A休眠2s,线程C休眠3s,则线程C在线程A之后执行,则 A Happens-Before C,所以C能及时 得到 A 修改的值,但是这个值存在主内存吗?如果存在主内存,线程B一直在while循环为什么取不到?

大家的疑惑是不是越来越深?对,其实由于Happens-Before规则,其 线程A 修改的值,已经刷新到了主内存,但是线程B的工作内存中一直在使用 map对象的修改数据之前的副本,这个副本的数据是过期的,

所以这就存在了可见性的问题,那怎么能让线程B,及时的读到主内存中的值呢,加volatile,加volatile为什么可以呢?因为编译时会给操作volatile的值加locks,加locks来保证什么呢?,缓存一致性协议

那就来讲讲缓存一致性协议:

缓存一致性协议大致是这样的,如果一旦CPU修改了volatile修饰的map对象的值,其他cpu缓存中的对象都无效,需要从主内存里面重新去加载

所以在线程B 执行这个时候String ssss = (String) map.get("a");放到CPU上去执行的时候,前期  map一直没有修改,所以是旧数据,但是一旦被别的线程修改了,并且刷新到主内存了,这个map就是过期的对象了,CPU会重新去主内存加载最新的值,这就达到了内存可见性

但是讲到这里大家一定还会有疑惑,为什么线程A,线程B,线程C 的引用都是一样的,为什么还是会不一样?

因为线程执行最终会给CPU去执行,CPU就会根据对象的引用去主内存中加载数据,把加载的数据放到L1,L2,L3等缓存中,如果下次执行,如果缓存中有,会直接从缓存里面去取,当线程A修改完数据之后,L1/2/3里面的数据都是过期,根据缓存一致性协议,如果缓存里面的数据的过期的,其会直接取主内存中取。现在应该明白了把

现在应该理解我们平时在开发过程中,如果没有涉及共享变量的操作,我们是无需去管线程并发的安全问题,因为,每个线程都在自己的工作空间内执行,不会相互影响

那缓存一致性协议底层到底什么实现的呢?

来看下支持缓存一致性协议的MESI协议

MESI协议:是以缓存行(缓存的基本数据单位,在Intel的CPU上一般是64字节)的几个状态来命名的(全名是Modified、Exclusive、 Share or Invalid)。该协议要求在每个缓存行上维护两个状态位,使得每个数据单位可能处于M、E、S和I这四种状态之一,各种状态含义如下:

       M:被修改的。处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
        E:独占的。处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改,即与内存中一致。
        S:共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
        I:无效的。本CPU中的这份缓存已经无效。

     一个处于M状态的缓存行,必须时刻监听所有试图读取该缓存行对应的主存地址的操作,如果监听到,则必须在此操作执行前把其缓存行中的数据写回CPU。
        一个处于S状态的缓存行,必须时刻监听使该缓存行无效或者独享该缓存行的请求,如果监听到,则必须把其缓存行状态设置为I。
        一个处于E状态的缓存行,必须时刻监听其他试图读取该缓存行对应的主存地址的操作,如果监听到,则必须把其缓存行状态设置为S。

       当CPU需要读取数据时,如果其缓存行的状态是I的,则需要从内存中读取,并把自己状态变成S,如果不是I,则可以直接读取缓存中的值,但在此之前,必须要等待其他CPU的监听结果,如其他CPU也有该数据的缓存且状态是M,则需要等待其把缓存更新到内存之后,再读取。

       当CPU需要写数据时,只有在其缓存行是M或者E的时候才能执行,否则需要发出特殊的RFO指令(Read Or Ownership,这是一种总线事务),通知其他CPU置缓存无效(I),这种情况下性能开销是相对较大的。在写入完成后,修改其缓存状态为M。

存在两种不能使用缓存一致性的情况

1、共享对象超过一个缓存行的大小,跨多个缓存行,那一个缓存行多大呢?

看只有64字节 

这里说下指令缓存,JVM执行引擎会把字节码转化成机器码,也就是汇编指令,CPU会加载需要执行的机器码放到指令缓存中

2、CPU不支持缓存一致性协议

如果不能使用缓存一致性,需要怎么做呢?加总线锁,注意这里总线锁,只是锁住总线里面 被volatile对象在主内存中引用不能被调用,也就是说所有的CPU只能有一个CPU能通过总线从内存中读/写数据、

这样也就会造成性能问题。

 

有序性

有序性,就是禁止指令重排,在修改的共享变量读写前后加内屏屏障,禁止编译器,解析器重排

指令重排发生的地方有两个:

1、java编译成字节码文件class的时候

2、执行引擎把class文件中的字节码指令转化成机器码的时候

【懒汉式-加双重校验锁&防止指令重排序的懒汉式】

public class LazyshiTest {

    public static void main(String[] args) {
        LazyshiSington lazyshiSington1 = LazyshiSington.getInstance();
        LazyshiSington lazyshiSington2 = LazyshiSington.getInstance();
        System.out.println(lazyshiSington1);
        System.out.println(lazyshiSington2);
        System.out.println(lazyshiSington1 == lazyshiSington2);
    }
}
class LazyshiSington{
    private volatile static LazyshiSington lazyshiSington = null; // volatile 禁止指令码重排序
    private LazyshiSington(){
        if(lazyshiSington != null){
            throw new RuntimeException("已经存在对象");    //防止反射通过构造器创建对象
        }
    }
    public static LazyshiSington getInstance(){
        if(lazyshiSington == null){  //a
            synchronized(LazyshiSington.class){  //b
                if(lazyshiSington == null){    //双重检测  //c
                    lazyshiSington  = new  LazyshiSington();   //d
                }
            }
        }
        return lazyshiSington;
    }
}

在回答这个问题之前你需要知道的知识点
instance=new  LazyshiSington();这个语句不是一个原子操作,编译后会多条字节码指令:

步骤1.为new出来的对象开辟内存空间
步骤2.初始化,执行构造器方法的逻辑代码片段
步骤3.完成instance引用的赋值操作,将其指向刚刚开辟的内存地址
可能场景-线程t1,t2均到达 代码b处
这个时候假若线程t1获得锁,t2处于阻塞状态,直到t1 依次执行代码a,b,c,d,并且在释放锁之前会将对变量instance的修改刷新到主存当中,保证当其他线程再进入的时候,在主存中读取到的就是最新的变量内容了。
t1释放锁之后,t2获得锁,根据重新从主内存拿到的变量instance值判断不为null,则直接跳过代码d的执行,即线程2只执行了代码a,b,c就释放掉了锁。
结论:这个场景下线程t1,t2会拿到了一个完整的instance所以是不存在问题的。

真正的问题场景-线程t1执行到代码d处,线程t2执行到代码a处
线程t1执行到代码d处时,在没有加volatile关键字修饰instance时是存在指令重排序的问题的,假若代码d的执行顺序是步骤1、步骤3、步骤2。
在线程t1执行完成步骤3,还没有执行步骤2时,线程t2执行到代码a处,对instance进行判断是否为null,发现不为null则直接返回使用(但此时instance是不一个不为null的但是没有初始化完成的对象)
结论:这个场景下线程t1是没有问题的会得到一个完整的instance,但是t2会提前拿到了一个不完整的instance是存在问题的,所以需要加上volatile来禁止这个语句instance=new  LazyshiSington();进行指令重排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值