volatile关键字的作用和注意事项

117 阅读3分钟

1、作用

volatile关键字在Java中主要用于保证内存可见性和禁止指令重排序

  1. 可见性:当一个线程修改了一个volatile变量,所有其他线程对该变量的读取都会看到最新的值。这是因为写操作会将变量的值写入主内存,而读操作会从主内存中获取最新值。
  2. 有序性:volatile还提供了一种称为“happens-before”的保证,这有助于维持代码的执行顺序,防止编译器和处理器为了优化而进行不必要的重排序。

2、使用volatile关键字解决多线程可见性的问题

可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存


public class TestVolatile {
//    private static volatile boolean stop = false;
    private static boolean stop = false;

    public static void main(String[] args) {
        // Thread-A
        new Thread("Thread A") {
            @Override
            public void run() {
                while (!stop) {
                }
                System.out.println(Thread.currentThread() + " stopped");
            }
        }.start();

        // Thread-main
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread() + " after 1 seconds");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop = true;
    }
}

当不使用volatile现象:

image.png

当不是用 volatile关键字修饰 stop变量时,当主线程 设置stop = false时,子线程A并不会停止,会一直运行下去

结论:

这是因为stop变量没有被volatile修饰,所以主线程修改stop变量的值后,子线程A无法感知到主线程修改stop变量的值,从而无法跳出循环,程序无法停止

当使用volatile运行结果:

image.png 当sopt=true时,子线程跳出循环,程序结束。

3、volatile只能保证多线程单次读写原子性

volatile可以保证变量的值会直接写入到主存中,对于其他线程来说,每次都是获取最新的值,因此可实现单次读写的原子性。但是对于i++这种看似一个操作,其实包含多次读写的操作,并不能保证其原子性,下面这个例子可以很好的证明:

public class VolatileTest01 {
    volatile int i;

    public void addI(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        final  VolatileTest01 test01 = new VolatileTest01();
        for (int n = 0; n < 1000; n++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test01.addI();
                }
            }).start();
        }
        Thread.sleep(10000);//等待10秒,保证上面程序执行完成
        System.out.println(test01.i);

    }
}

现象:

每次计算的的结果都小于1000

结论:

这是因为 volatile只能保证单次读或单次写的原子性 ,而 i++ 操作:会有三个步骤(并不是单次操作):

  1. 读取i的值
  2. 将i的值加1
  3. 将i的值写入主存 尽管volatile可以确保每次读写都是同步的,并且最新的值对所有线程都可见,但它无法保证整个i++操作作为一个整体不被中断。在多线程环境中,如果两个线程几乎同时尝试执行i++,它们都可能读取到相同的i值,各自独立地将其加1,然后写回新值。这样,i的值只会增加1,而不是预期的2。