如何理解Java中volatile关键字

发布时间:2021-10-09 14:39:00 作者:iii
来源:亿速云 阅读:101

这篇文章主要介绍“如何理解Java中volatile关键字”,在日常操作中,相信很多人在如何理解Java中volatile关键字问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何理解Java中volatile关键字”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

CPU缓存

缓存模型

计算机中的所有运算操作都是由CPU完成的,CPU指令执行过程需要涉及数据读取和写入操作,但是CPU只能访问处于内存中的数据,而内存的速度和CPU的速度是远远不对等的,因此就出现了缓存模型,也就是在CPU和内存之间加入了缓存层。一般现代的CPU缓存层分为三级,分别叫L1缓存、L2缓存和L3缓存,简略图如下:

如何理解Java中volatile关键字

如何理解Java中volatile关键字

缓存的出现,是为了解决CPU直接访问内存效率低下的问题,CPU进行运算的时候,将需要的数据从主存复制一份到缓存中,因为缓存的访问速度快于内存,在计算的时候只需要读取缓存并将结果更新到缓存,运算结束再将结果刷新到主存,这样就大大提高了计算效率,整体交互图简略如下:

如何理解Java中volatile关键字

缓存一致性问题

虽然缓存的出现,大大提高了吞吐能力,但是,也引入了一个新的问题,就是缓存不一致。比如,最简单的一个i++操作,需要将内存数据复制一份到缓存中,CPU读取缓存值并进行更新,先写入缓存,运算结束后再将缓存中新的刷新到内存,具体过程如下:

这样的i++操作在单线程不会出现问题,但在多线程中,因为每个线程都有自己的工作内存(也叫本地内存,是线程自己的缓存),变量i在多个线程的本地内存中都存在一个副本,如果有两个线程执行i++操作:

这个就是典型的缓存不一致问题,主流的解决办法有:

总线加锁

这是一种悲观的实现方式,具体来说,就是通过处理器发出lock指令,锁住总线,总线收到指令后,会阻塞其他处理器的请求,直到占用锁的处理器完成操作。特点是只有一个抢到总线锁的处理器运行,但是这种方式效率低下,一旦某个处理器获取到锁其他处理器只能阻塞等待,会影响多核处理器的性能。

缓存一致性协议

图示如下:

如何理解Java中volatile关键字

缓存一致性协议中最出名的就是MESI协议,MESI保证了每一个缓存中使用的共享变量的副本都是一致的。大致思想是,CPU操作缓存中的数据时,如果发现该变量是一个共享变量,操作如下:

具体来说,MESI中规定了缓存行使用4种状态标记:

有关MESI详细的实现超出了本文的范围,想要详细了解可以参考此处或此处。

JMM

看完了CPU缓存再来看一下JMM,也就是Java内存模型,指定了JVM如何与计算机的主存进行工作,同时也决定了一个线程对共享变量的写入何时对其他线程可见,JMM定义了线程和主内存之间的抽象关系,具体如下:

简略图如下:

如何理解Java中volatile关键字

MESI类似,如果一个线程修改了共享变量,刷新到主内存后,其他线程读取工作内存的时候发现缓存失效,会从主内存再次读取到工作内存中。

而下图表示了JVM与计算机硬件分配的关系:

如何理解Java中volatile关键字

并发编程的三个特性

文章都看了大半了还没到volatile?别急别急,先来看看并发编程中的三个重要特性,这对正确理解volatile有很大的帮助。

原子性

原子性就是在一次或多次操作中:

一个典型的例子就是两个人转账,比如A向B转账1000元,那么这包含两个基本的操作:

这两个操作,要么都成功,要么都失败,也就是不能出现A账户扣除1000但是B账户金额不变的情况,也不能出现A账户金额不变B账户增加1000的情况。

需要注意的是两个原子性操作结合在一起未必是原子性的,比如i++。本质上来说,i++涉及到了三个操作:

这三个操作都是原子性的,但是组合在一起(i++)就不是原子性的。

可见性

另一个重要的特性是可见性,可见性是指,一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。

一个简单的例子如下:

public class Main {
    private int x = 0;
    private static final int MAX = 100000;
    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        Thread thread0 = new Thread(()->{
            while(m.x < MAX) {
                ++m.x;
            }
        });

        Thread thread1 = new Thread(()->{
            while(m.x < MAX){
            }
            System.out.println("finish");
        });

        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        thread0.start();
    }
}

线程thread1会一直运行,因为thread1x读入工作内存后,会一直判断工作内存中的值,由于thread0改变的是thread0工作内存的值,并没有对thread1可见,因此永远也不会输出finish,使用jstack也可以看到结果:

如何理解Java中volatile关键字

有序性

有序性是指代码在执行过程中的先后顺序,由于JVM的优化,导致了代码的编写顺序未必是代码的运行顺序,比如下面的四条语句:

int x = 10;
int y = 0;
x++;
y = 20;

有可能y=20x++前执行,这就是指令重排序。一般来说,处理器为了提高程序的效率,可能会对输入的代码指令做一定的优化,不会严格按照编写顺序去执行代码,但可以保证最终运算结果是编码时的期望结果,当然,重排序也有一定的规则,需要严格遵守指令之间的数据依赖关系,并不是可以任意重排序,比如:

int x = 10;
int y = 0;
x++;
y = x+1;

y=x+1就不能先优于x++执行。

在单线程下重排序不会导致预期值的改变,但在多线程下,如果有序性得不到保证,那么将可能出现很大的问题:

private boolean initialized = false;
private Context context;
public Context load(){
    if(!initialized){
        context = loadContext();
        initialized = true;
    }
    return context;
}

如果发生了重排序,initialized=true排序到了context=loadContext()的前面,假设两个线程A、B同时访问,且loadContext()需要一定耗时,那么:

volatile

好了终于到了volatile了,前面说了这么多,目的就是为了能彻底理解和明白volatile。这部分分为四个小节:

先来介绍一下volatile的语义。

语义

volatile修饰的实例变量或者类变量具有两层语义:

如何保证可见性以及有序性

先说结论:

下面分别进行介绍。

可见性

Java中保证可见性有如下方式:

具体来说,可以看一下之前的例子:

public class Main {
    private int x = 0;
    private static final int MAX = 100000;
    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        Thread thread0 = new Thread(()->{
            while(m.x < MAX) {
                ++m.x;
            }
        });

        Thread thread1 = new Thread(()->{
            while(m.x < MAX){
            }
            System.out.println("finish");
        });

        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        thread0.start();
    }
}

上面说过这段代码会不断运行,一直没有输出,就是因为修改后的x对线程thread1不可见,如果在x的定义中加上了volatile,就不会出现没有输出的情况了,因为此时对x的修改是线程thread1可见的。有序性

JMM中允许编译期和处理器对指令进行重排序,在多线程的情况下有可能会出现问题,为此,Java同样提供了三种机制去保证有序性:

另外,关于有序性不得不提的就是Happens-before原则。Happends-before原则说的就是如果两个操作的执行次序无法从该原则推导出来,那么就无法保证有序性,JVM或处理器可以任意重排序。这么做的目的是为了尽可能提高程序的并行度,具体规则如下:

对于volatile,会直接禁止对指令重排,但是对于volatile前后无依赖关系的指令可以随意重排,比如:

int x = 0;
int y = 1;
//private volatile int z;
z = 20;
x++;
y--;

z=20之前,先定义x或先定义y并没有要求,只需要在执行z=20的时候,可以保证x=0,y=1即可,同理,x++y--具体先执行哪一个并没有要求,只需要保证两者执行在z=20之后即可。

原子性

Java中,所有对基本数据类型变量的读取赋值操作都是原子性的,对引用类型的变量读取和赋值也是原子性的,但是:

也就是说,volatile并不能保证原子性,例子如下:

public class Main {
    private volatile int x = 0;
    private static final CountDownLatch latch = new CountDownLatch(10);

    public void inc() {
        ++x;
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        IntStream.range(0, 10).forEach(i -> {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    m.inc();
                }
                latch.countDown();
            }).start();
        });
        latch.await();
        System.out.println(m.x);
    }
}

最后输出的x的值会少于10000,而且每次运行的结果也并不相同,至于原因,可以从两个线程A、B开始分析,图示如下:

如何理解Java中volatile关键字

也就是说,多线程操作的话,会出现两次自增但是实际上只进行一次数值修改的操作。想要x的值变为10000也很简单,加上synchronized即可:

new Thread(() -> {
    synchronized (m) {
        for (int j = 0; j < 1000; j++) {
            m.inc();
        }
    }
    latch.countDown();
}).start();

实现原理

前面已经知道,volatile可以保证有序性以及可见性,那么,具体是如何操作的呢?

答案就是一个lock;前缀,该前缀实际上相当于一个内存屏障,该内存屏障会为指令的执行提供如下几个保障:

使用场景

一个典型的使用场景是利用开关进行线程的关闭操作,例子如下:

public class ThreadTest extends Thread{
    private volatile boolean started = true;

    @Override
    public void run() {
        while (started){
            
        }
    }

    public void shutdown(){
        this.started = false;
    }
}

如果布尔变量没有被volatile修饰,那么很可能新的布尔值刷新不到主内存中,导致线程不会结束。

synchronized的区别

到此,关于“如何理解Java中volatile关键字”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

推荐阅读:
  1. java中的volatile关键字是什么?volatile关键字怎么用?
  2. 深入理解volatile关键字

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

java volatile

上一篇:Web应用中GoTTY终端工具的安装以及用法

下一篇:Linux中性能监控和优化命令分别是哪些呢

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》