• Java并发--volatile关键字


    一、volatile的实现原理

    synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁,而volatile就可以说是JVM提供的最轻量级的同步机制。JMM告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写入主内存中?这个实际对普通变量没有规定的,而针对volatile修饰的变量给Java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读,从而保证数据的可见性。

    被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读现象

    在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候回多出Lock前缀的指令,主要有两个方面的影响:

    将当前处理器缓存行的数据写回系统内存;

    这个写回内存的操作会使得其他CPU 里缓存了该内存地址的数据无效,当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

    这样volatile变量通过这样的机制就是的每个线程都能获得该变量的最新值

    二、volatile能保证线程安全吗--原子操作(i++)

    volatile并不能保证线程安全。volatile关键字保证可见性、有序性。单不保证原子性。

    可见性

    对一个volatile变量的读,总能看到(任意线程)对这个volatile变量的写入。

    原子性

    对任意单个volatile变量的读/写具有原子性,单类似于volatile++这种复合操作不具有原子性

    多线程下自增

    很多人认为,多线程下i++这个是多线程并发问题,在变量count之前加上volatile就可以避免这个问题,看看结果是不是复合我们的预期。

    package passtra;
    
    public class Conter{
        
        public volatile static int count=0;
        
        public static void inc(){
            try {
                Thread.sleep(1);
            } catch (Exception e) {
                // TODO: handle exception
            }
            count++;
        }
        
        public static void main(String[] args) {
            for(int i=0;i<1000;i++){
                new Thread(new Runnable() {
                    
                    @Override
                    public void run() {
                        Conter.inc();
                        
                    }
                }).start();
            }
            System.err.println("运行结果:Counter.cont="+Conter.count);
        }
    }

    运行结果:Counter.cont=980

    运行结果不是我们期望的1000,每次运行的结果也不相同

    原因分析

    这是因为虽然volatile保证了内存可见性,每个线程拿到的值都是最新值,但是count++这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。所以每个线程最终赋值是会进行重复赋值

    1、JVM运行时内存区域,其中有一个内存区域是JVM虚拟机栈,每个线程运行时都有一个线程栈(线程私有)

    线程栈保证了线程运行时变量值信息

    2、当线程访问某一个对象值得时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体指load到线程本地内存中,建立一个变量副本。

    之后线程就不在和对象在堆内存中的变量值有任何关系了,而是直接修改副本变量的值

     3、在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值写回到对象在堆中变量

    这样在堆中的变量的值就发生了变化

    4、交互图如下:

    • read and load:从主存复制3变量到当前工作内存
    • use and assign:执行代码,改变共享变量值
    • store and write:线程本地工作内存数据刷新主存相关内容

    其中:

    1、use and assgin可以多次出现,但是这些操作并不是原子性的,也就是在read load之后,如果主内存count变量发生修改后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果和预期不会一样。

    2、对于volatile修饰的变量,jvm只是保证从主内存加载到线程工作内存的值是最新的,例如:

    线程A,线程B在进行read,load操作中,发现内存中count的值都是5,那么都会加载这个最新的值;

    在线程A对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6;

    线程B对由于已经进行read load操作,在进行运算之后,也会更新主内存count的变量值为6;

    导致两个线程及时使用volatile关键字修改之后,还会存在并发的情况

    解决办法

    1、可以使线程串行执行(其实就是单线程,没有发挥多线程的优势)

    2、可以使用synchronized或者锁的范式保证原子性

    3、使用Atomic包中的AtomicInteger来替换int,它利用CAS算法保证了原子性

    三、volatile的防止指令重排应用--双重懒加载单利模式

    package passtra;
    
    public class Singleton{
        
        private static volatile Singleton singleton;
        
        private Singleton(){}
        
        public static Singleton getsingleton(){
            
            if(singleton==null){
                synchronized (Singleton.class) {
                    if(singleton==null){
                        singleton=new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

    这里的volatile关键字就是为了防止指令重排。

    如果不用volatile,singleton=new Singleton();这段代码其实分了三步:

    分配内存空间(1)

    初始化对象(2)

    将singleton对象指向分配的内存地址(3)

    加上volatile是为了让这三步操作顺序执行,反之有可能第二部在第三部之前执行,就有可能某个线程拿到的单利对象是还没有初始化的,以至于报错。

    四、volatile在Java并发中的应用

    volatile在Java并发中用的很多。比如:Atomic包中的value,以及AbstractQueuedLongSynchronizer中的state都是被定义为volatile来保证内存可见性

  • 相关阅读:
    Nacos启动异常:failed to req API:/api//nacos/v1/ns/instance after all servers([127.0.0.1:8848])
    多节点集群思路
    内网dns配置
    MySQL集群配置思路
    pycharm常用快捷键
    2020年11月新版CKA考试心得
    JavaScript的Map、Set、WeakMap和WeakSet
    AJAX传输二进制数据
    linux性能监测与优化的指令
    八千字硬核长文梳理Linux内核概念及学习路线
  • 原文地址:https://www.cnblogs.com/houqx/p/13508163.html
Copyright © 2020-2023  润新知