代码改变世界

关于volatile的探索

2020-12-27 11:11  CeddieCoding  阅读(84)  评论(0)    收藏  举报

在阅读SynchronousQueue源码时,发现有这么一段注释:

// Note: item and mode fields don't need to be volatile
// since they are always written before, and read after,
// other volatile/atomic operations.

大意是item和mode这两个属性不需要用volatile修饰,因为他们总在其他的volatile/atomic操作之前/后被写入/读取。

关于volatile的可见性原理在这里就不详细展开说了,大致是因为CPU三级缓存的存在,各个线程在运行时读取或写入变量时,并未实际读取或写入内存中,而是在操作缓存中的数据,导致线程间共享变量状态不一致。此修饰符会在变量被读之前加入load barrier,在被写入之后加入store barrier,以便其他线程可以读取到。

上段注释中的释义也很清晰,即如果在volatile变量写入之前写入,在volatile变量读取之后读取,那么就会“享受”到volatile的便利性,show me the code:

public class TestVolatile {
    /*volatile int obj = 0;*/
    boolean running = false;

    boolean get() {
        return running;
    }

    void doSetTrue() {
        running = true;
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile instance = new TestVolatile();
        instance.producer();
        Thread.sleep(100);
        instance.consumer();
    }
    private void producer() {
        Thread t = new Thread(() -> {
            while (
              /*this.obj != -1 &&*/ // volatile读
              !this.get()) {
                doNothing();
            }
            System.out.println("Thread 1 finished.");

        });
        t.start();
    }

    private void consumer() {
        Thread t = new Thread(() -> {
            this.doSetTrue();
            /*this.obj = 1;*/  // volatile写
            System.out.println("Thread 2 finished.");
        });
        t.start();
    }

    private void doNothing() {
        // do nothing
    }
}

被注释掉的两行分别为volatile的读和写。此段代码的执行结果为:

Thread 2 finished.

并且Thread1一直处于循环状态之中,因为在JIT优化下,指令会被重排序,导致Thread1执行的代码变成以下形式:

if(!this.get()) {
    while(true) {
        ...
    }
}

在第一次读取缓存中的值为false时,就会进入到while(true)的死循环内。有以下两种方式改善这种情况:

①-Djava.compiler=NONE:

这种方式简单粗暴,直接禁用了JIT的优化,指令不会被重排序

②volatileg关键字:

如果把被注释掉的部分放开,那么执行结果为:

Thread 2 finished.
Thread 1 finished.

综上可知,即便running不是volatile变量,只要其读写次序在volatile变量的前后,那么也会享受到volatile变量带来的便利性,因为volatile已经强制同步、刷新了缓存,并且防止了指令重排序,普通变量在多线程环境下也自然会被正确的读和写。