Java并发编程(七)——volatile与Java内存模型(JMM)

172 阅读3分钟

在前文中已经简单介绍了Java内存模型,JMM是围绕着原子性、可见性和有序性展开的。为了在适当的场合确保线程间的原子性、有序性和可见性,Java使用了一些特殊的操作和关键字来申明:不要在这个地方随意对指令进行优化或变动。volatile关键字就是其中之一。

当你用volatile申明一个变量时,就等于告诉虚拟机这个变量极有可能会被其他线程修改。为确保这个变量被修改后,应用程序范围内所有线程都能够”看到“这个改动,这时虚拟机就需要采取一些特殊手段来保证这个变量的可见性等性质。

在之前文章中关于原子性的讲解中说到了并发写入long类型数据时会发生的问题。在这种情况下,怎么样才能保证数据的原子性呢?最简单的一种方法就是引入volatile声明。有些文章中说volatile只保证可见性而并不保证原子性,但是在Oracle的文档中说道:”Reads and writes are atomic for all variables declared volatile (including long and double variables).“这是怎么一回事呢?其实两者都是正确的,Oracle文档中"write"的意思是“赋值”,即类似i=100000L这种操作,当多个线程同时进行这种单步赋值操作时,i的值不会被写乱;而文章中描述的原子性是复合操作的原子性。

public class PlusTask implements Runnable{
    static volatile int i = 0;
    @Override
    public void run(){
        for (int k = 0; k < 10000; k++) {
            i++;
        }
    }
    
    public static void main(String[] args) throws InterruptedException{
        Thread[] threads = new Thread[10];
        for (int j = 0; j < 10; j++) {
            threads[j] = new Thread(new PlusTask());
            threads[j].run();
        }
        
        for (int j = 0; j < 10; j++) {
            thread[j].join();
        }
        
        System.out.println(i);
    }
}

执行上述代码,如果第6行i++是原子性的,那么最终的值应该是100000,但实际上上述代码的输出几乎总是会小于100000,这是因为i++实际上是分读取i的值、i的值增加1、i赋值为新值这些步骤的,而volatile并不能保证多条指令间的原子性,所以要保证复合指令的原子性仍然需要借助同步锁机制。

volatile也可以保证数据的可见性和有序性。

public class NoVisibility {
    private static boolean ready;
    private static int number;
    
    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while(!ready) {
                System.out.println(number);
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException{
        new ReaderThread().start();
        Thread.sleep(1000);
        number = 42;
        ready = true;
        Thread.sleep(10000);
    }
}

在虚拟机的Client模式下,由于没有JIT做优化,在主线程修改readytrue后,ReaderThread线程可以发现这个改动,继而打印变量后退出。但在Server模式下,由于系统优化,ReaderThread无法看到主线程中的修改,导致循环永远无法退出。这就是一个典型的可见性问题。这时我们只需要将ready变量声明为volatile,告诉JVM这个变量可能在不同线程中修改,这样就可以顺利解决这个问题了。