JUC学习之volatile可见性

一、简介

volatile,是Java中的一个关键字。被volatile修饰的变量,在多个线程中保持可见性,注意,volatile不保证原子性,这也是volatile与synchronized的区别之一。

那么什么是可见性呢?

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

要了解volatile的工作原理,首先需要了解一下Java内存模型,下面是粗略图:

在Java中,每个线程都有一个独立的内存空间,称为工作内存。 它保存了用于执行操作的不同变量的值。在执行操作之后,线程将变量的更新值复制到主内存中,这样其他线程可以从那里读取最新值。

二、使用场景 - 保证内存可见性

如果需要保证某个变量在多个线程之间可见性的时候,可以使用volatile关键进行修饰。

示例:

public class T03_Volatile {
    /**
     * 如果没有加volatile修饰,线程A由于死循环,可能没有及时从主内存读取最新的running值
     * 加了volatile修饰,一旦running的值发生变化,就会通知其他线程需要从主内存重新获取值
     */
    private volatile boolean running = true;

    private void m1() {
        while (running) {
            System.out.println("hello....");
        }
    }

    public static void main(String[] args) {
        T03_Volatile t03_volatile = new T03_Volatile();
        new Thread(t03_volatile::m1, "A").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t03_volatile.running = false;
    }
}

当然,有可能出现没加volatile关键字,线程A也能及时读取到最新的running值,这主要是由于CPU可能暂时空闲,自动从主内存同步了最新的running到线程A。

三、使用场景 - 禁止指令重排序

指令重排序,简单理解就是说,保证代码按照我们写的顺序执行。被volatile修饰了的变量,禁止了指令进行重排序,所以可以保证代码完全按照我们编写的顺序执行(不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的)。

比较典型的例子就是双重检查机制的单例模式编写:

public class T03_Volatile {
    /**
     * 加入volatile关键字修饰
     */
    private volatile static T03_Volatile instance = null;

    private T03_Volatile() {
    }

    public static T03_Volatile getInstance() {
        if (null == instance) {
            synchronized (T03_Volatile.class) {
                if (null == instance) {
                    instance = new T03_Volatile();
                }
            }
        }
        return instance;
    }
}

由于 instance = new T03_Volatile(),实际上可分为三个步骤:

//分配对象的内存空间
//初始化对象
//设置instance指向刚分配的内存地址

操作2依赖1,但是操作3不依赖2,所以有可能出现1,3,2的顺序,当出现这种顺序的时候,虽然instance不为空,但是对象也有可能没有正确初始化,会出错。

四、使用场景 - 不保证原子性

volatile关键字不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。

示例:

public class T03_Volatile {
    private static CountDownLatch countDownLatch = new CountDownLatch(500);
    private volatile static int num = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MICROSECONDS.sleep(400);
                    num++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num = " + num);
    }
}

运行结果:

多运行几次,发现结果不固定,有时候会是500,有时候是495,有时候是497,这就证明了volatile不保证原子性。

原因解析:

num++操作并不是原子操作,实际上num++的操作分成了三个步骤进行:

//获取变量的值
int temp = num;
//将该变量的值+1
num = num + 1;
//将该变量的值写回到对应的主内存中
num = temp;

举一个例子:

假设线程A首次拿到的num = 3,在执行+1操作前,可能存在其他多个线程已经对num做了修改,假设此时主内存中的num已经被修改到20了,而此时线程A执行+1操作,将num=3+1=4的结果又重新写回到了主内存中,将原本num应该是num = 20 + 1 = 21的,覆盖成了4,这就存在了原子性问题。

五、总结

volatile关键字保证内存可见性,禁止指令重排序,但是注意它不保证原子性,所以volatile不能完全替代synchronized关键字,因为synchronized保证原子性的。

发布了220 篇原创文章 · 获赞 93 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/Weixiaohuai/article/details/104543438