美文网首页
并发线程-volatile关键字

并发线程-volatile关键字

作者: 一只狗被牵着走 | 来源:发表于2020-09-06 18:05 被阅读0次

    1、结论

    volatile具有可见性防止指令重排的能力,但是在某些场景下不能保证线程安全(无法替代synchronized关键字)

    2、原因简析

    1、线程安全问题中有三个概念:原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)。
    2、synchronized关键字可以保证原子性、可见性和有序性;volatile只能保证可见性和有序性(有序性体现在 防止指令重排 上)。
    3、使用volatile修饰的(多线程共享的)变量进行的是原子的修改操作时,这时volatile可以保证线程安全;除此之外,单一地使用volatile不保证线程安全。
    4、volatile会对总线(主存)加上LOCK前缀指令(观察汇编源码得知),LOCK不是内存屏障,但是完成的事情是类似内存屏障(也叫内存栅栏)的功能。LOCK可以理解成是CPU一级的锁,加上LOCK后,其他CPU对该内存地址的原子的读写请求都会被阻塞,直到锁释放。(《码出高效Java开发手册》P232中描述使用了volatile后“...任何对此变量的操作都会在内存中进行,不会产生副本”,笔者认为描述有问题)
    5、单一地使用synchronized(来保证线程安全)会有一定的效能损耗,可以用volatile搭配使用synchronized减少(因为要保证线程安全带来的)效能损耗,也可以搭配CAS(比如自旋锁运用了CAS -- Compare-And-Swap)。

    3、背景

    计算机在对内存进行操作时,会存在主内存(有些地方叫物理内存)和高速缓存的概念。主存中的变量值对所有线程可见,高速缓存是线程私有的--对其他线程不可见的。CPU对内存进行操作的时候,单个线程会从主存(总线)中读取目标内存地址中的数据,copy到高速缓存(作为副本),后续的一系列操作都是基于这个“副本”,操作完后,将副本的值同步回主存。
    内存栅栏实现了 可见性 和 防止指令重排 的效果


    内存栅栏/内存屏障

    4、代码验证

    4.1、这是一段线程不安全的代码

    public class Test {
    
        public static volatile int inc = 0;
    
        public static void increase() throws InterruptedException {
            inc++;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread thread1 = new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++){
                        try {
                            increase();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("----A过程---" + Test.inc);
                    }
                }
    
            });
            Thread thread2 = new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++){
                        try {
                            increase();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("----B过程---" + Test.inc);
                    }
                }
    
            });
            thread1.start();
            thread2.start();
            Thread.sleep(2000);
            System.out.println("--终态--" + inc);
        }
    }
    

    4.2、对#4.1代码的优化

    ·#4.1的代码不能复现出问题,猜测可能是机器的CPU性能较好。所以优化了下代码,如下

    public class Test {
    
            public static volatile int inc = 0;
    
            public static void increase() throws InterruptedException {
                Thread.sleep(1);
                inc ++;
            }
    
            public static void main(String[] args) throws InterruptedException {
    
    
                for (int k=0;k<10;k++){
                    new Thread(new Runnable(){
                        @Override
                        public void run() {
                            for (int i=0 ; i < 500; i++){
                                new Thread(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            Thread.sleep(2);
                                            increase();
                                        } catch (InterruptedException e) {
                                            e.printStackTrace();
                                        }
                                        System.out.println("----A过程---" + inc);
                                    }
                                }).start();
                            }
                        }
    
                    }).start();
                    new Thread(new Runnable(){
                        @Override
                        public void run() {
                            for (int i=0 ; i < 500; i++){
                                new Thread(new Runnable() {
                                    @Override
                                    public void run() {
                                        try {
                                            Thread.sleep(2);
                                            increase();
                                        } catch (InterruptedException e) {
                                            e.printStackTrace();
                                        }
                                        System.out.println("----B过程---" + inc);
                                    }
                                }).start();
                            }
                        }
    
                    }).start();
                }
                Thread.sleep(5000);
                System.out.println("--终态--" + inc);
            }
    }
    

    4.3、#4.2的运行结果

    预期结果是10,000,实际运行结果<10,000

    4.4、原因

    因为#4.1和#4.2的模型一样,#4.1的逻辑更简单,故以#4.1为例讲

    4.4.1、首先,问题出在这一行


    inc++

    4.4.2、其次,inc++非原子操作


    inc++ 即 inc = inc + 1
    4.4.3、出现异常(结果不合预期)的情况
    step-1 step-2 step-3 step-4 step-5

    4.5、反思

    从结果看来,在这个场景中,volatile没有发挥任何作用嘛?
    我们去掉#4.2代码中的volatile关键字,发现结果也是少于10,000


    没有volatile修饰inc变量的情况

    我认为,volatile还是发挥作用的(只是没有它没有让结果达到预期),举个例子

    去掉volatile后,step-4的线程B不是无效掉前两步的操作,而是将自己的副本(inc=2)更新到主存中,这时主存中的inc值又被更新了一次(2 -> 2);
    假设在线程竞争中,线程B获得的CPU时间片轮远少于线程A时,当线程A对inc更新过好几轮了后(假设此时主存中的inc=4),线程B仍然对主存更新为2。这时主存中的inc值经历了几个阶段


    主存中inc的几个阶段

    4.6、比较明确地体现volatile的可见性作用的例子

    1、状态标记量

    volatile boolean flag = false;
     
    while(!flag){
        doSomething();
    }
     
    public void setFlag() {
        flag = true;
    }
    

    2、双重检测锁

    class Singleton{
        private volatile static Singleton instance;
         
        private Singleton() {}
         
        public static Singleton getInstance() {
            if(instance==null) {
                synchronized (Singleton.class) {
                    if(instance==null)
                        instance = new Singleton();
                }
            }
            return instance;
        }
    }
    

    5、volatile适用的场景

    1)对变量的写操作不依赖于当前值

    2)该变量没有包含在具有其他变量的不变式中

    针对这两点约束,个人还不是很理解,具体参考# volatile的适用场景

    6、参考来源

    1、 Java并发编程:volatile关键字解析
    2、 volatile 和 内存屏障
    3、《码出高效Java开发手册》

    相关文章

      网友评论

          本文标题:并发线程-volatile关键字

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