Java中的volatile简介

Java内存模型的抽象结构

Java线程之间的通信由Java内存模型(JMM,Java Memory Model)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

这样的内存模型设计会产生可见性问题。可见性的意思是指当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。但在多线程环境下,很容易产生不可见的问题,例如下面代码:

public class VisibilityTest {

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();

        while (true) {
            if (thread.isFlag()) {
                System.out.println("Yes!");
                break;
            }
        }
    }

}

class MyThread extends Thread{

    private boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("Set flag to true!");
    }
}

你会发现,上述代码永远不会输出Yes!,这便是产生了内存可见性问题。有很多方式来保证内存可见性,volatile便是其中一种。

volatile的内存语义

volatile保证了变量的可见性,其内存语义如下:

  • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。注意,是所有的共享变量,而非仅仅是volatile变量自身。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,然后从主内存中读取共享变量。

我们把上述代码中的共享变量用volatile修饰,再看运行结果如何:

public class VisibilityTest {

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();

        while (true) {
            if (thread.isFlag()) {
                System.out.println("Yes!");
                break;
            }
        }
    }

}

class MyThread extends Thread{

    private volatile boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("Set flag to true!");
    }
}

运行结果:

Set flag to true!
Yes!

Process finished with exit code 0

如何正确使用volatile

相对于锁,volatile不会造成线程阻塞,也更易于阅读,但如果使用不当,volatile可能不会给你提供预想的线程安全,这主要是因为volatile对复合操作不具备原子特性。例如,a++,实际上涉及a的读取、a的自增和a的写入三个操作,所以此时如果有多个线程执行a++操作,不能够保证线程安全,请看下面的代码:

public class VolatileTest {
    private static volatile int sum = 0;
    private static CountDownLatch latch = new CountDownLatch(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            MyThread myThread = new MyThread();
            myThread.start();
        }
        latch.await();
        System.out.println(sum);
    }

    static class MyThread extends Thread {

        @Override
        public void run() {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
            latch.countDown();
        }
    }
}

上述代码的输出结果永远是一个小于等于10000的不确定的值。


每日学习笔记,写于2020-05-03 星期日

你可能感兴趣的:(Java中的volatile简介)