并发线程-volatile关键字

2020-09-06  本文已影响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开发手册》

上一篇下一篇

猜你喜欢

热点阅读