美文网首页
Volatile关键字

Volatile关键字

作者: 竖起大拇指 | 来源:发表于2019-12-31 15:13 被阅读0次

    Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引用了同步块和volatile关键字机制。而关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉。

    synchronized

    同步块大家都比较熟悉,通过synchronized关键字实现,所有加上synchronized块的语句,在多线程访问的时候,同一时刻只能有一个线程能够执行synchronized修饰的方法或者代码块。

    volatile

    下面看一个例子,我们实现一个计数器,每次线程启动的时候,调用计数器inc方法,对计数器进行加1.

    public class Counter {
        public static int count = 0;
    
        public static void inc() {
            //这里延迟1毫秒,使得结果明显
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
            count++;
        }
    
        public static void main(String[] args) {
            //同时启动100个线程,去进行i++计算,看看实际结果
            for (int i = 0; i < 100; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Counter.inc();
                    }
                }).start();
            }
            //这里每次运行的值都有可能不同,可能为100
            System.out.println("运行结果:Counter.count=" + Counter.count);
        }
    }
    

    很多人认为,这个是多线程并发问题,只需要在变量count之前加上volatile就可以避免这个问题,那么我们修改代码看看。

    public class Counter {
     
        public volatile static int count = 0;
     
        public static void inc() {
     
            //这里延迟1毫秒,使得结果明显
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
     
            count++;
        }
     
        public static void main(String[] args) {
     
            //同时启动100个线程,去进行i++计算,看看实际结果
     
            for (int i = 0; i < 100; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Counter.inc();
                    }
                }).start();
            }
     
            //这里每次运行的值都有可能不同,可能为100
            System.out.println("运行结果:Counter.count=" + Counter.count);
        }
    }
    

    运行结构还是没有达到我们的期望,下面我们分析一下原因。
    在JVM运行时刻内存分配有一个区域就是JVM虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象值的时候,首先通过对象的引用栈找到对象在堆内存的变量的值,然后把堆内存变量的值load到线程本地内存中,建立一个变量副本,之后线程就不在和对象在堆内存变量值有任何关系,而是直接修改副本变量的值。在修改完成之后的某一时刻(线程退出之前),自动把线程变量副本的值会写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图很好地描叙了这个交互过程


    image.png
    1.read and load 从主存复制变量到当前工作内存
    2.use and assign 执行代码 改变共享变量值
    3.store and write 用工作内存数据刷新主存相关内容

    其中use and assign可以出现多次
    但是这一些操作并不是原子性,也就是在read load 之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
    例如假如线程1,线程2 都对 count 值进行加一操作,假设一开始 count 的值是 5,那么最后正确的结果应该是 7,但事实却不是这样的。

    在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值 5。在线程1对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6。线程2由于已经进行 read,load 操作,在进行运算之后,也会更新主内存count的变量值为6。此时,导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。

    Volatile的应用场景

    一般volatile用来在多线程环境变量,它能保证在读取的那个时候读取到的数据都是最新的数据。例如我们有3个线程,有一个count为0.当在1秒钟的时候,线程1,2同时去读取count的值,之后线程1,2分别加1,然后休眠一段时间,此时线程1,2中的count值3,而主内存中的count值为0。当线程1,2还在休眠的时候,线程3去读取count的值。如果count没有用关键字volatile修饰,那么线程3将直接读取0作为其值。但是如果count用volatile修饰,则JVM将会重新读取最新的值3,并将其会写主存,然后线程3在读取最新的值。所以volatile只能保证你获取数据的那个时候的数据是最新的,但是它并不能保证线程并发带了的数据覆盖等问题。

    总结:

    总的来说 Volatile 可以保证一个线程上对某个变量的修改对于另外一个变量来说是可见的。因此如果所有线程对某变量的修改都不依赖与之前的值。比如:我们有一个 flag 的变量,所有线程都只是对该变量置 true 或 false,那么Volatile 可以很好地完成这项工作。但是如果对该变量的修改依赖于之前的值,比如之前的累加,那么 Volatile 并不能避免并发带来的数据覆盖问题。

    volatile与synchronzied的区别

    volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
    volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
    volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
    volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
    volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

    Volatile如何保证内存可见性?

    1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
    2.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量

    相关文章

      网友评论

          本文标题:Volatile关键字

          本文链接:https://www.haomeiwen.com/subject/jviboctx.html