美文网首页Android技术
Java 中 synchronized 与 volatile 区

Java 中 synchronized 与 volatile 区

作者: 杰哥长得帅 | 来源:发表于2018-03-18 09:22 被阅读34次

    volatile 和 synchronized 特点

    首先需要理解线程安全的两个方面:执行控制内存可见

    执行控制 的目的是控制代码执行(顺序)及是否可以并发执行

    内存可见 控制的是线程执行结果在内存中对其它线程的可见性。根据 Java 内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU 缓存),操作完成后再把结果从线程本地刷到主存

    synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都 happens-before 于随后获得这个锁的线程的操作

    volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求

    使用volatile关键字仅能实现对原始变量,如 boolen、short、int、long 等操作的原子性,但需要特别注意,volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况

    volatile 实现可见性

    volatile 通过加入内存屏障和禁止重排序优化来实现的:

    • 对 volatile 变量执行写操作时,会在写操作后加入一条 store 屏障指令
    • 对 volatile 变量执行读操作时,会在读操作前加入一条 load 屏障指令

    重排序:

    在虚拟机层面,为了尽可能减少内存操作速度远慢于 CPU 运行速度所带来的 CPU 空置的影响,虚拟机会按照自己的一些规则将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用 CPU

    通俗的讲:volatile 变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的线程总能看到该变量的最新值

    线程写 volatile 变量的过程:
    1. 改变线程工作内存中 volatile 变量副本的值
    2. 将改变后的副本的值从工作内存刷新到主内存
    线程读 volatile 变量的过程:
    1. 从主内存中读取 volatile 变量的最新值到线程的工作内存中
    2. 从工作内存中读取 volatile 变量的副本
    要在多线程中安全的使用 volatile 变量,必须同时满足:
    1. 对变量的写入操作不依赖其当前值,或者能确保只有单个线程更新变量的值
    2. 该变量没有包含在具有其他变量的不变式中

    第一个原则的理解就是,当你需要改变这个变量时,要保证你要改变的值跟这个变量原先的值没有任何关系,比如:

    count = 10;
    

    这里无论 count 的值是什么都直接赋值了 10,这个变化跟它原先的值没有任何关系,如果是如下的方式:

    count ++;
    

    这里 count 的结果是依赖于它原来的值加 1 得到的,所以这种场景不适合使用 volatile 关键字

    从 Java 内存模型的角度理解:被 volatile 关键字修饰的变量只能保证assgin -> store -> write操作和read -> load -> use操作的原子性,但count ++操作包括的原子操作有:read -> load -> use -> assgin -> store -> write操作,所以自加操作并非一个原子操作,线程 A 在读取到count的最新值之后,在assgin操作之前可能切换到线程 B,线程 B 此时执行的操作可能为read -> load -> use -> assgin -> store -> write操作,完成了count的自加操作,此时线程 A 由于已经读取到count的值,所以不再从主存中刷新count的值,但此时线程 A 的工作内存中保存的count的值已经过期,线程 A 对过期的count值进行自加操作后写会了主内存,从而造成数据的错误

    第二个原则的理解,我们举如下例子:

    public class NumberRange {
        private volatile int lower, upper;
    
        public int getLower() { return lower; }
        public int getUpper() { return upper; }
    
        public void setLower(int value) { 
            if (value > upper) 
                throw new IllegalArgumentException(...);
            lower = value;
        }
    
        public void setUpper(int value) { 
            if (value < lower) 
                throw new IllegalArgumentException(...);
            upper = value;
        }
    }
    

    这里我们看到,setLowersetUpper两个方法中,使用了大小的边界检查,保证了lower总是小于upper,在单线程环境下没有问题,但是如果有另外两个线程并发的调用setLowersetUpper,比如,初始状态是(0, 5),某个时刻线程 A 调用setLower(4)的同时线程 B 调用setUpper(3),这个时候两个调用都可以通过边界检查,最后得到(4, 3),这个边界结果显然是没有意义的,但是 volatile 在这里并不能起作用,这种情况应该使用锁来保证边界结果的有效性

    volatile 和 synchronized 的区别

    • volatile本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住
    • volatile修饰变量;synchronized修饰方法
    • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
    • volatile仅能实现变量的修改可见性,不能保证原子性,因为一个线程 A 修改了变量还没结束时,另外的线程 B 可以看到已修改的值,而且可以修改这个变量,而不用等待 A 释放锁,因为volatile变量没上锁。而synchronized则可以保证变量的修改可见性和原子性

    相关文章

      网友评论

        本文标题:Java 中 synchronized 与 volatile 区

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