当前位置: 首页 > 后端技术 > Java

JDK成长笔记13:(好深入的文)你能从三个层面分析一下volatile的底层原理吗?(Part1)

时间:2023-04-01 14:27:54 Java

前面几节你应该已经了解了Thread和ThreadLocal的底层原理。在接下来的几节中,让我们探索volatile的基本原理!不知道大家有没有这样的感受:很多工程师都很难解释volatile关键字的作用或者原理。比如有些人根本不知道volatile的功能和应用场景;例如,有些人不知道什么是顺序、可见性和原子性。例如,有些人可能会说它的作用是什么“保证秩序”。无法保证属性、可见性、原子性。”但大多数人很难解释为什么不能保证有序性、可见性和原子性;比如在面试的时候,经常被面试官问到volatile的时候,回答的时候支支吾吾,思路不清晰,答不出一个满意的答案。诸如此类等等的场景还有很多……要弄清楚这些东西,可不是一件容易的事情。那么在接下来的《JDK源码成长记-并发篇》中,我将带领大家一步步探索volatile的奥秘,解决这些尴尬的场景,熟练使用和理解volatile关键字。你好VolatileHelloVolatile

首先你要了解的第一点就是的,这里使用volatile的时候,要记住以下两点:1.当多个线程读写同一个变量时2.当多个线程需要保证顺序和可见性的时候,我们来看看这两个分别点:volatile的第一种使用场景:多个线程读写同一个变量时。您可以通过HelloVolatile示例来理解这一点。代码如下:publicclassHelloVolatile{//可见性示例privatestaticvolatilebooleanshouldRunning=true;//一个线程修改后,另一个线程无法读取修改后的值,线程间内存数据不可见//privatestaticbooleanshouldRunning=true;publicstaticvoidmain(String[]args){newThread(()->{System.out.println("读取变量shouldRunning="+HelloVolatile.shouldRunning);while(HelloVolatile.shouldRunning){}System.out.println("运行结束,读取变量shouldRunning="+HelloVolatile.shouldRunning);}).start();newThread(()->{try{System.out.println("ModifyVariable");Thread.sleep(1000);HelloVolatile.shouldRunning=false;}catch(InterruptedExceptione){}}).start();}}上面的代码显然是两个线程。线程1在while循环中使用shouldRunning判断是否跳出循环,线程2修改shouldRunning。这是一个典型的一读一写的场景。画个图让大家更好的理解:这个用法看起来很简单,但实际上在很多开源框架的底层都是通过这种方式来控制线程执行的。学完volatile我再给大家举几个例子。volatile的第二种使用场景:当需要保证顺序和可见性的时候。这两点我们后面会慢慢研究。上面的例子,如果不加volatile修改shouldRunning变量,线程2修改值后,线程1不可见,不会跳出循环。如果你想了解顺序性,这里有一个经典的例子。在线程安全的单例(DLC-doublechecklock)场景下,volatile的重要作用就是保证有序性。另一点要提到的是,volatile保证了顺序和可见性。并不是说HelloVolatile中没有顺序保证。为大家找到了SpringCloudEureka组件中的configurationmanager创建,是使用DCL的单例。代码如下:publicclassConfigurationManager{staticvolatileAbstractConfigurationinstance=null;publicstaticAbstractConfigurationgetConfigInstance(){if(instance==null){synchronized(ConfigurationManager.class){if(instance==null){instance=getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));}}}返回实例;}}了解了volatile,我们再回头看看这个使用volatile来保证有序性的DCL。就在这里给大家一个印象吧。什么是秩序、可见性、原子性?什么是顺序、可见性、原子性?
上面说的有序性,有些人可能不清楚可见性和原子性的含义。我什至不明白它是如何保证的,原理是什么。而如果你想了解volatile如何保证顺序和可见性,你首先需要了解什么是顺序、可见性和原子性。这里我就不深入解释了,只用一句话给大家做一个简单的概述。可见性,一句话,就是当多个线程读写同一个变量时,线程之间可以互相知道,也就是可见性。有序性,一句话,就是因为代码执行的顺序可能会被重新排序,volatile可以保证代码行数按顺序执行。原子性,一句话,就是当多个线程同时写入同一个变量时,只有一个线程可以执行这个操作。以上三句话你可能都看过了,但是还不是很理解。没关系。学完volatile,你可以回来再看一遍这三句话。下面我们就这三点由浅入深地探讨一下。主要层次如下:1.JVM内存模型和Java内存模型(JMM)层次2.JVM指令层次和JVM中的C++源代码层次3.CPU缓存模型+硬件结构原理+CPU指令层次JVM内存结构和概念评论JMMJVM内存结构与JMM概念回顾
简单来说一句话:就是刷新主内存,强制工作内存过期其他线程的话中的主内存和工作内存是Java内存模型中的概念,要理解Java内存模型(JMM),就必须知道JVM的内存结构(运行时内存区),这里放几张图让大家回顾一下JVM的内存结构和JMM的概念首先我们来回顾一下JVM的内存结构,如下图所示:如果你认识上图的JVM同学,那你一定不陌生,看不懂也没关系,这里简单介绍一下,大家可以理解:JVM的内存区,或者说运行时数据区,简单的分为两个区域:堆和栈。除了堆内存之外,每个线程共享的区域还有方法区的概念。不同的JVM版本对方法区的实现是不同的。JDK1.8方法区的实现称为MetaSpace元数据空间,用于存放加载到JVM内存中的类的基本信息和数据。堆内存是创建的Java对象,一般分配到堆内存,Heap区。这两个公共内存区域可以被所有线程访问。它们的具体作用如下:堆(Heap):由线程共享。所有对象实例和数组都必须在堆上分配。收集器主要管理Object。方法区:由线程共享。存储类信息、常量、静态变量、即时编译器编译的代码。方法栈(JVMStack):线程私有。存储局部变量表、操作栈、动态链接、方法出口、对象指针。本地方法栈(NativeMethodStack):线程私有。为虚拟机使用的Native方法服务。比如Java使用c或者c++写的接口服务时,代码就运行在这个区域。程序计数器(ProgramCounterRegister):线程私有。有的文章也被翻译成PCRegister(PCRegister),一样的东西。可以看作是当前线程执行的字节码的行号指示符。指向下一条要执行的指令。从颜色可以看出,除了线程共享的内存区域外,每个线程都有自己独有的内存区域,如程序计数器、本地方法栈、Java方法虚拟机栈等。这是线程独有的内存区域,不会被其他线程访问。下面我给大家简单介绍一下JMM。它的逻辑模型如下图所示:上图可以看出比较抽象,因为JMM本身就是一个内存模型的抽象,不是一个实际的结构,而是对应的具体实现和具体结构。大家都知道很多东西在计算机层面都会被抽象出来。比如网络的分层模型等等。在Java中,准确的说JVM在内存中的抽象概念是JMM,也就是Java的内存模型。这种抽象可以对应具体的JVM组件或者具体的硬件组件。对应关系可以理解为下图:上面说的JVM内存结构其实就是图的左边,说明和JVM的对应关系就是堆和元数据空间可以看成是主存,而Java方法虚拟机栈,程序计数器等都可以看作是自己的工作内存。右边对应的其实可以对应CPU的L1-L3缓存,缓存区,writebuffer等可以看作是JMM中各个线程的工作内存,实际物理内存可以看作是JMM中的主内存JMM。内存,线程共享的区域。从JMM的角度来看,volatile是如何保证可见性的呢?从JMM的角度,volatile如何保证可见性?</div>回顾了JVM内存结构和JMM内存模型,我们从这两个层面来分析一下volatile是如何保证可见性的。首先是JMM级别。在JMM中,定义了一些操作和规则以确保可见性。这里我们深入的说说JMM的知识,只说一下我们会用到的知识。首先,让我们谈谈操作。JMM规定了8个原子操作,用来描述主存和工作的操作动作和操作原理。JMM指令1)锁(lock):作用于主存中的变量,将一个变量标识为线程独占状态。2)解锁(unlock):作用于主内存变量,释放一个处于锁定状态的变量,释放后的变量可以被其他线程锁定。3)读取(read):作用于主存变量,将一个变量值从主存传送到线程的工作内存,以便后续加载动作使用4)加载(load):作用于变量工作内存的变量副本,它将从主内存读取操作得到的变量值放入工作内存的变量副本中。5)use(使用):作用于工作内存的变量,将工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到需要使用该变量值的字节码指令时,执行这个操作.6)assign(赋值):作用于工作内存的变量,它将从执行引擎接收到的一个值赋值给工作内存的变量,每当虚拟机遇到赋值给的字节码指令时执行这个操作变量。7)store(存储):作用于工作内存的变量,将工作内存中的一个变量的值传送到主存中,供后续的写操作使用。8)写(write):作用于主存的变量,它将store操作从工作存中的一个变量的值转移到主存的变量中。JMM的指令使用规则不允许读取和加载、存储和写入操作之一单独发生。即使用read也要load,用store也必须write。不允许线程丢弃其最新的分配操作。即工作变量的数据发生变化后,必须通知主存不允许线程将未赋值的数据从工作内存同步回主存。内存新变量必须在主内存中创建,工作内存不允许直接使用未初始化的变量。即在对变量实现use和store操作之前,必须先通过assign和load对其进行操作。只有一个线程可以同时锁定一个变量。多次加锁后,必须进行相同次数的解锁才能解锁。如果一个变量被锁定,这个变量在所有工作内存中的值将被清除。在执行引擎使用这个变量之前,必须重新加载或赋值初始化变量的值。如果变量未锁定,则无法解锁。您无法解锁被其他线程锁定的变量。在解锁变量之前,必须将变量同步回主存。执行,不允许遗漏或乱序read-->load-->use,assign-->store-->write是允许的;每个变量的加锁操作只允许一个线程同时执行多次,并且只有执行了相同次数的解锁后才能释放该变量;最新的数据在释放锁之前写入主内存,最新的数据在进入锁之前读入工作内存。注意这8个操作并不完全对应CPU和JVM实现的指令,后面我们分析JVM指令的时候会看到。他们这些。JMM内存模型解释HelloVolatile如下图所示:通过JMM的一些操作和原理,使用volatile可以保证不同线程的工作内存发送读写时的变量可见性。volatile保证可见性的原理还是之前总结的一句话:写入主存数据时,刷新主存值后,其他线程的工作内存被强制过期。底层是加锁和解锁操作的原理造成的。其他线程读取变量时,必须重新加载主存中最新的数据,从而保证可见性。好了,到这里你应该明白了volatile的基本作用和可见性的原理,明白了JMM、JVM和volatile的关系。下一节我们将继续深入研究如何在JVM指令级和C++代码级通过内存屏障和CPU锁前缀指令来保证可见性和有序性。本文由博客多发平台OpenWrite发布!