volatile和线程安全、原子性、有序性、可见性

  • Post author:
  • Post category:其他


一、

线程安全要考虑三个方面:可见性、有序性、原子性



可见性指,一个线程对共享变量修改,另一个线程能看到最新的结果



有序性指,一个线程内代码按编写顺序执行



原子性指,一个线程内多行代码以一个整体运行,期间不能有其它线程的代码插队

二、

volatile


能够保证共享变量可见性与有序性,但并不能保证原子性



原子性举例


下面代码有一个共享变量balance=10,有两个线程一个-5,一个+5,后台打印出来是10。但如果运行上万次,就有可能不是这个结果。这样的代码虽然只有一句,并不是原子性的操作,我们从控制台打开,运行javap -p -v AddAndSubtract.class,查看他的字节码,发现相加方法或者相减方法的代码是多个字节码指令组成的。

public class AddAndSubtract {

    static volatile int balance = 10;

    public static void subtract() {
        int b = balance;
        b -= 5;
        balance = b;
    }

    public static void add() {
        int b = balance;
        b += 5;
        balance = b;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(2);
        new Thread(() -> {
            subtract();
            latch.countDown();
        }).start();
        new Thread(() -> {
            add();
            latch.countDown();
        }).start();
        latch.await();
        LoggerUtils.get().debug("{}", balance);
    }
}

如下,cpu是在线程中高速切换的,如果发生以下交叉,输入的值是一个错误的结果

/**
 t1 10
 0: getstatic 读取静态变量
                 t2
                 0: getstatic 10 读取静态变量
                 3: iconst_5 准备数字5
                 4: isub  相减
                 5: putstatic 设置静态变量
                    5
 3: iconst_5 准备数字5
 4: iadd 相加
 5: putstatic 设置静态变量
 15

 */



可见性举例


以下代码foo方法检测stop的值,如果一直为假则i不断相加,设置一个线程,在程序100ms后把stop设置为true,看是否能够成功停止。

public class ForeverLoop {
    static volatile boolean stop = false;

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            stop = true;
            get().debug("modify stop to true...");
        }).start();

        foo();
    }

    static void foo() {
        int i = 0;
        while (!stop) {
            i++;
        }
        get().debug("stopped... c:{}", i);
    }
}

从下面运行图得知,stop已经设置,但程序并没有停止。

我们先来看看网上的一个分析,这个说法是错误,这个解释是说变量的值并没有同步到内存中,那我们在写一个线程,在程序运行的0.2s后读取stop的值。

我们发现线程1是读取到了stop修改后的值,说明还是已经存入到内存,不然它是不会读取到的,他上图的说法不攻自破了。我们来详细解释一下这个可见性问题

所有的程度都交给cpu执行,cpu去查看线程1代码里的stop值是什么,根据他的值决定下一步操作,他到物理内存中读到stop的值,第一次他读到的是false,他不断的高速循环,经过测试,0.1s这个读取次数到达上万次,这么多次到物理内存都是false,内存的读写效率是比较低的,这时候JVM里面java即时编译器JIT,他负责代码的优化,任何一条java代码都会翻译成字节码指令,但是他还不能直接交给cpu执行,他还有一个解释器组件,他会将字节码逐行翻译成机器码,再交给cpu执行。JIT来对一些热点的字节码进行优化,反复进行的代码就是热点代码,这个循环次数超出他的优化阈值,他对这个while循环进行了一个很大的优化,直接将stop替换成了false。那问题就来了,线程2改了stop的值,线程1完全不知情,因为机器码已经被替换了。



有序性举例

通过第三方插件进行数亿次的压力测试,来体现这个效果。actor1是线程1给xy复制,而actor2是线程2读取xy值。他的值可能有很多种组合,看控制台输出结果。

volatile不同位置会影响压力测试结果,volatile相当于一个内存屏障,让他上面的代码无法排到他下面去,读取x的屏障也不能越到读取上去y



版权声明:本文为weixin_43162044原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。