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内存结构和概念评论JMM
JVM内存结构与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是如何保证可见性的呢?