Java并发编程之关键字volatile的深入解析

时间:2022-01-14 23:12:38

 

前言

volatile是研究Java并发编程绕不过去的一个关键字,先说结论:

volatile的作用:

        1.保证被修饰变量的可见性

        2.保证程序一定程度上的有序性

        3.不能保证原子性

下面,我们将从理论以及实际的案例来逐个解析上面的三个结论

 

一、可见性

 什么是可见性?

举个例子,小明和小红去看电影,刚开始两个人都还没买电影票,小红就先去买了两张电影票,没有告诉小明。小明以为小红没买,所以也去买了两张电影票,因为他们只有两个人,所以他们只能用两张票,这就是小明和小红他俩电影票的数量的可见性。

在讲解之前,我们简单的了解一下JVM当中运行时数据区的结构

Java并发编程之关键字volatile的深入解析

堆内存:存放的就是对象,所以它也是JVM当中内存最大的一区域

线程私有区:线程中的栈会去从堆当中获取变量的值来进行操作,正是因为是私有化的,所以两个线程之间的数据是不会共享的

元空间:存放静态变量以及常量还有被虚拟机加载的类信息

同理,我们可以将小明和小红看作java当中的两个线程1和2,共有一个变量

public class volatileTest {
    public static boolean flag = false;
 
    public static void main(String[] args) {
        try {
            new Thread(() -> {
                System.out.println("线程1开始");
                //线程1当中取反值,当flag为true时才会跳出循环
                while (!flag) {
                }
                System.out.println("线程1结束");
            }).start();
            Thread.sleep(100);
            new Thread(() -> {
                System.out.println("线程2开始");
                //线程2给flag赋值
                flag = true;
                System.out.println("线程2结束");
            }).start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

该代码的运行结果如下:

Java并发编程之关键字volatile的深入解析

可以很清楚的看到,只有线程2是跑完了的,但是明明线程2已经给flag赋值,线程1并没有停止循环,这就是flag这个变量没有可见性,导致线程1一直不停止

解决的方法有两种

第一:让每个线程空余时间就去堆同步数据(显然不合理)

第二:使用volatile关键字去修饰变量flag

让我们加上volatile试试:

Java并发编程之关键字volatile的深入解析

这回线程1总算是成功停止了,由此我们可得,volatile是可以让变量具有可见性的。

学习编程不能只知道如何去使用,而是要知道原理,这样才会有更多的薪资

那么volatile的底层是如何实现的呢?

如上面jvm运行数据区的图所示,所有的变量都是存在了堆当中,而每个线程都是拿到他们的副本进行计算和修改,volatile干了啥事呢,如下图所示

Java并发编程之关键字volatile的深入解析

这里我们介绍一个新的概念,叫总线(各位可以把它理解成进行连接线程和堆内存,在计算机的硬件当中,也是有总线的,了解的朋友可以把它用相同概念理解一下)。

当一个被volatile修饰的变量,在某一个线程当中被修改时,总线会监听到这个变动,并且会让其他线程中的这个变量失效,简而言之,当线程2当中堆flag进行了修改,则会导致线程1当中的flag失效,就是把这个线程1当中的flag删了。当线程1中没有flag了,它会重新去获取flag,这个时候,就会使我们的变量flag具有了可见性。

现在我们已经知道了,volatile的实行原理,那么它的底层是如何实现的?

众所周知,java语言加载时  -> class  ->汇编语言 -> 机器语言,因为volatile是个关键字,所以它的底层是一种汇编语法,被volatile修饰的变量其实就是给它加了个一个lock前缀指令。

也就是说,当面试官问到我们,如何手写一个volatile时,我们可以说在编译的层面,添加一个lock前缀指令相当于一个内存屏障,它本身会提供三个功能

        1)它会强制堆缓存的修改操作立即写入主存

        2)如果是写操作,它会导致其他CPU中对应的缓存行无效

        3)它会确保指令重排序时不会吧其它的指令排到内存屏障之前的位置,也不会之前的操作拍到内存屏障之后

前面两点很好理解,并且我们也进行了进一步的认证,第三点可能有朋友不太明白,这就引出了我们下一个论点,volatile可以保证一定的有序性

 

二、有序性

我们看下面三行代码

int i=1;
int j=2;
i =i++;

在我们的理解当中,程序时自上而下运行的,先是第一行,再是第二行等,然而事实上,jvm可能会对代码进行重排序,比如它可能就会让上面的这三行代码变成下面的状态

int i=1;        
i =i++;
int j =2;

为什么会进行重排序,目的是让代码执行的速度更快,当然它也不是随便乱排的,排序的规则是根据代码的依赖性进行的判断,简而言之就是在不影响结果的情况下进行排序,感兴趣的朋友可以自行去了解一下

这是java本身对程序保证的有序性,在不影响运行结果的情况下进行重排序,但是仅限于单线程的情况下,在多线程的情况中,并不能有效地保证程序的有序性

下图为手写的一个单例模式,不做过多的赘述,左边为代码,右边为翻译的字节码文件

Java并发编程之关键字volatile的深入解析

通过上图可以很清晰的看出,new OnlyObject这个操作重点分为了四步,

                第一步:创建这个对象

                第二步:调用这个类的构造方法

                第三步:添加指向(就是从私有线程当中执行堆)

                第四步:加载

由于java对程序的重排序,会使第二步和第三步进行调换位置,在单线程当中不会有任何问题,而在多线程当中就有问题了

看下图代码

Java并发编程之关键字volatile的深入解析

当线程1已经完成添加指向时,在堆当中其实已经分配了一个值,但是这时并没有调用构造方法,所以导致此时这个对象只是一个半成品对象 ,里面并不是我们想要的值。这时线程2走进来,他发现object并不为空,所以直接返回了,此时的程序跟我们的业务并不相符,所以我们需要使用volatile来保证我们的有序性。

 

总结

到此这篇关于Java并发编程之关键字volatile的文章就介绍到这了,更多相关Java并发编程关键字volatile内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/m0_51464746/article/details/120175951