zoukankan      html  css  js  c++  java
  • 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来保证内存可见性

  • 相关阅读:
    POJ 2236 Wireless Network(并查集)
    POJ 2010 Moo University
    POJ 3614 Sunscreen(贪心,区间单点匹配)
    POJ 2184 Cow Exhibition(背包)
    POJ 1631 Bridging signals(LIS的等价表述)
    POJ 3181 Dollar Dayz(递推,两个long long)
    POJ 3046 Ant Counting(递推,和号优化)
    POJ 3280 Cheapest Palindrome(区间dp)
    POJ 3616 Milking Time(dp)
    POJ 2385 Apple Catching(01背包)
  • 原文地址:https://www.cnblogs.com/houqx/p/13508163.html
Copyright © 2011-2022 走看看