这个关键字的挥发性可能听说过许多朋友。它具有两个重要的功能:确保可见性和禁止说明。但是我们对使用动荡及其背后的原则一无所知,因此本文将带您良好的理解。
由于挥发性关键字与Java的内存模型有关,然后在告诉挥发性的钥匙之前,让我们首先了解Java内存模型,然后引入挥发性关键字的使用,最后解释挥发性关键字的原理。我们毫无疑问。直接输入文本。
一旦共享变量(类的成员变量,类的静态成员变量)会通过挥发性修改,然后具有两层语义:
挥发性保证可见性的可见性。如果线程A首先执行该线程,则执行线程B:
此代码是典型的代码。许多人在中断线程时可能会采用此标记。但是实际上,此代码会正确运行吗?它会被线程中断吗?不一定,也许大多数时候,此代码可能会中断线程,但也可能导致线程无法中断(尽管这种可能性很小,只要发生这种情况,它将导致死周期)。
以下解释了为什么此代码可能导致无法中断的线程。它早些时候已经解释了每个线程在操作过程中都有其自己的工作内存,因此当线程A运行时,它将在其自身中复制oftreqrequsted变量的值工作记忆。
因此,当线程B更改止回变量的值,但没有时间写入主内存时,线程B已转向其他事物,因此,线程A不会知道soprequested变量的更改,sogo down。
上面的代码将stoprequest定义为挥发性,并且成为典型的状态标记情况。
当变量定义为挥发性时,它将具有以下特征:确保该变量对所有线程的可见性。此处的“ visibleness”是指修改线程时此变量的值。新值是针对其他线程的其他线程。它可以立即学习。特别是,挥发性关键字可以确保直接从主内存中读取变量。如果修改了此变量,它将始终将其写回主内存。通过将新值同步回到主内存,可以通过修改后的变量来可见Java内存模型。在变量读取之前,它依赖于主内存的主要内存值。
普通变量和挥发性变量之间的差异是:挥发性的特殊规则可确保新值可以立即同步到主内存,并且在每次使用挥发性变量之前,请立即从主内存中刷新每个线程。可以说,波动性保证了多线程操作期间变量的可见性,而普通变量不能保证这一点。
在此示例中,在线程B更改optrequest变量的值之后,新值将立即写回主内存。当螺纹A再次读取停止的变量时,主内存会读取。
关于挥发性变量的可见性,开发人员通常会误解它们。他们会错误地认为以下描述是正确的:“所有线程都可以立即可见挥发性变量。换句话说,挥发性变量在各种线程中是一致的,因此基于挥发性变量的操作是在同时发生的。“这句话的论点部分不是错误的,但是不能得出结论,即“基于挥发性变量的操作在并发下是安全的”。Java中的操作运算符并不是原子操作,这会导致挥发性变量。同时发生的不安全。
挥发性无法保证JMM文章中提到的原子能是挥发性不能保证原子能,我们将通过案例进行分析。
以上代码很简单。两个线程后,相同的共享整数变量执行100,000加1操作。我们希望计数的价值将在最终印刷中打印。计数的值很可能不等于200,000,并且每个操作的结果都不同,始终小于200,000。?
自我启动操作没有原子性。它包括阅读变量的原始值,执行1个操作和编写工作记忆。
如果特定时刻的变量计数值为10,则
线程A执行变量的自我提示操作。线程A首先读取变量计数的原始值,然后阻止螺纹A(可能存在);
然后,线程B执行自我启动操作,线程B还读取变量计数的原始值。由于线程A仅读取变量计数并且不修改变量,因此主内存中计数的值不会更改,因此线程B将直接进入主内存以读取计数的值,它是发现计数的值为10,然后添加1个操作,将11写入工作内存,最后将其写入主内存。
然后将螺纹A添加到1个操作中。由于读取了计数的值,因此请注意,目前螺纹A的工作内存中计数的值仍然为10,因此计数A的值将1添加到计数为11,然后写入11要工作记忆,最后写入主内存。
然后,在两个线程执行自我提示操作后,Inc仅增加了1。
在这里说明,一些朋友可能有疑问。不,不是要确保在修改变量以修改挥发性变量时立即知道变量?是的,这是正确的。这是上述规则中的挥发性变量规则:编写挥发性域,,,,编写一个挥发性域,,hapepens-之前将读取此波动性域。但是,应该注意的是,线程A读取变量后,如果被阻止,它不会修改计数值。尽管挥发性可以确保螺纹B在该变量上的值可变计数是从内存中读取的,线程A不会修改它,因此线程B根本不会看到修改值。
根本原因在这里。自我启动操作不是原子操作,并且挥发性无法保证该变量的任何操作是原子。
将上述代码更改为以下任何类型以实现效果:
同步关键字,伪代码如下:
锁定锁,代码如下:
除了上述两个方案外,我们还可以使用AtomicInteger来完成其他操作。
在JDK 1.5中提供一些Java.util.concurrent.Atomic下的原子操作类,封装减去操作(减一个数字),以确保这些操作是原子操作。
AtomicInteger类主要使用CAS(比较和交换) +挥发性和本机方法来确保原子操作,从而避免了同步的高费用,并且执行效率得到了极大的提高。CAS实际上是使用CMPXCHG指令实现的。处理器执行CMPXCHG指令是原子操作。
挥发性禁止说明在之前被排除在外,并提到挥发性的关键字可以禁止说明进行分类,因此挥发性可以在一定程度上保证命令。
挥发性关键字禁止指令分为两个含义:
我们分析了最经典示例中的分类问题。每个人都应熟悉单打模式的实现,在同时环境中的单个案例实现方法中,我们通常可以使用双检查锁(DCL)来实现它。源代码如下:
现在,让我们分析为什么在变量单例之间添加挥发性关键字。要了解此问题,我们必须首先了解对象的结构。实际上,一个物体实际上可以分为三个步骤:
(1)分配内存空间。
(2)初始化对象。
(3)将存储空间的地址分配给相应的参考。
但是,由于可以根据说明对操作系统进行分类,因此上述过程也可能成为以下过程:
(1)分配内存空间。
(2)将存储空间的地址分配给相应的参考。
(3)初始化对象
如果是这个过程,则可以将多线程环境暴露于未准备好的对象,这将导致不可预测的结果。因此,为了防止此过程对此过程进行分类,我们需要将变量设置为挥发性类型变量。
在上一篇文章中提到了可见性。线程本身不会与主内存直接与数据交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程之间无形数据的基本原因。如下所示:如下所示:
挥发性保证该变量对所有线程的可见性。此处的“可见性”是指修改线程时变量的值。新值可以知道其他线程。
理性下方:
挥发性使用锁定前缀指令禁止本地内存缓存,以确保不同线程之间的内存可见性。
在了解JMM的相关知识之后,我们知道为了提高处理速度,处理器不会直接与主内存通信。取而代之的是,将主内存数据读取到内部缓存(L1,L2或其他)。但是在操作后,我不知道何时将缓存数据写回主内存。如果声明波动性的变量,则JVM将向处理器发送锁定前缀指令,并且变量所在的缓存数据将立即写回主内存。其他处理器仍然很旧,计算操作会出现问题。因此,在多个处理器下,为了确保每个处理器的缓存是一致的,将实现缓存一致性协议。每个处理器通过嗅探在总线上扩散的数据来检查缓存的值。当处理器发现其自己的高速缓存线的内存地址已修改时,当前处理器的高速缓存线将设置为无效状态。当处理器修改数据时,数据将再次从主内存到数据再读取到数据到数据到数据。处理器缓存。
锁定前缀的说明将在多核处理器下引起两件事:
理解挥发性特征的一种好方法是将挥发性变量的单个读数同步/写作作为挥发性变量的单个读数/写作。发行挥发性写作和锁的发布具有相同的内存语义;挥发性读取相同的内存语义和锁的获取 - 这使得挥发性变量写入可以实现线程之间的通信。
挥发性的记忆语义:
挥发性写入 - 阅读内存语义:
如下所示:
JMM文章中的编译器和处理器的内容禁止禁止的说明。由于符合单个线程环境中数据依赖性的数据依赖性,编译器和处理器不会更改数据依赖项中存在的两个操作的执行。螺纹环境,重订单可能会导致准确的数据获得准确的数据。
首先,让我们看一下指令重分类对记忆可见性的影响:
如果可以对1到2、1和2之间的数据依赖关系进行排序(类似于3和4)。结果是,当读取线程B在4处执行时,您可能看不到共享变量的修改编写线程A被执行1。
挥发性禁止指令排序语义的关键是记忆障碍。
该测序可能会引起多线程程序的内存可见性问题。对于处理器的回归排序,分类规则的JMM处理器调节将要求Java编译器在生成指令序列时(内存屏障(Intel Call)(Intel Call)(Intel呼叫IT)指令,禁止通过内存屏障指令从特定类型中进行特定类型。处理器专注于分类。通过禁止特定类型的编译器对处理器进行分类和处理,它为程序员提供了一致的内存可见性保证。
为了确保内存的可见性,Java编译器将在指令序列的适当位置插入内存屏障指令,以禁止特定类型的处理器分类。
Storeload障碍物是一个“全面”障碍,具有其他三个障碍的作用。大多数现代的多种处理器都支持障碍物(其他类型的障碍不一定得到所有处理器的支持)。执行此障碍架空架空的执行费用非常昂贵,因为当前的处理器通常会将写作缓冲区中的数据刷新到内存(缓冲区完全齐平)。
JMM为编译器制定了挥发性餐厅:
为了实现挥发性的内存语义,编译器将在生成字节代码以禁止特定类型的处理器排序时将内存屏障插入指令序列中。
以下是基于保守策略的JMM内存屏障插入策略:
以下是在挥发性写入内存屏障之后生成的指令序列的示意图:
以下是保守策略下挥发性产生的指令序列的示意图。
从编译器的重分类规则和处理器记忆屏障插入策略的角度来看,只要挥发性变量和普通变量之间的重分类可能会破坏挥发性的记忆语义(可见记忆),则这种重型分类将通过编译器的处理器的记忆屏障插入策略被禁止。
上面阅读的挥发性和挥发性的记忆障碍插入策略是非常保守的。在实际执行中,只要挥发性写作阅读的记忆语义,编译器就可以根据特定情况忽略不必要的障碍。以下是特定的解释。示例代码。
对于ReadAndWrite方法,可以优化编译器如下:
请注意,最终的Storeload屏障无法省略。因为在写下第二次挥发性后,该方法立即返回。这次,编译器可能无法准确确定以后是否会有挥发性读数或写作。出于安全原因,编译器通常在此处插入Storeload屏障。
当挥发性修饰符和数组挥发性修饰符和数组仅确保其参考地址的可见性。
如以下代码所示,在数字添加挥发性后,将立即打印以下代码。如果您不在数组中添加挥发性,则永远不会打印它。
首先,有必要了解数组存储在主内存中。当对象访问线程时,数组将工作内存的副本引用到线程,甚至可以将NUM [0]复制到工作内存。:
根据对挥发性可见性的实现的分析,我们知道,执行执行语句时,数组引用将写回主内存,并且线程B工作记忆中的数组引用的缓存线将失败,这将导致从主内存中读取重新阅读以从主内存读取。EssenceBut要注意的一件事是:NUMS参考和NUMS [0]不在同一缓存中,因此NUMS [0] threads之间的可见性无法保证。
为了实时测试NUMS [0]的最新值,我们使用以下代码进行演示:
多次执行上述代码,观察结果变化,最后发现存在这种情况:
也许我的测试方法是不正确的,但是当我只想证明挥发性修改数组时,它不能保证数组元素之间的可见性。也可以确认这是ConturrentHashMap。在ConcurrentHashMap(1.8)中,表格的内部使用表来保存数据。小心的学生可以发现每次道格lea都使用数组的元素时,不安全类的getObjectVolas方法使用数组的元素。当设置数组元素时,使用compareAreAreAndSwapObject方法而不是直接通过竞标操作。是原因吗?
在互联网上查看文章是这样的摘要:因为在Java数组的元素层面上缺乏元数据设计,因此无法表示元素作为最终,挥发性的语义语义,因此打开后门,而GetObjectVolatile是用于补充不能表示为挥发性的元素。坑,@stable用于补充最终的凹坑,数组元素与挥发性的成员场相同,该元素无法保证线程之间的可见性。
关于挥发性修改对象的情况也有这样的情况,因此不仅要小心。
此外,您可以在互联网上看到这样的情况。感兴趣的朋友可以理解。R DA回答了自己,并解释了非常详细。
推荐阅读:System.Out语句将导致线程结束。如果删除了System.Out语句,则该线程将永远不会结束
挥发性关键字和同步关键字是Java并发学习中非常重要的知识点。无论您以前对它们学到了多大程度,因为您开始同时学习Java,都必须彻底了解其背后的实现原则。
以我为例,我开始从添加方法学习CopyOnwritearRaylist类中的挥发性。在此方法中,复制了数组,然后添加了新值,然后覆盖了原始数组。该数组通过挥发性修改。在我当时阅读的文章中,博客作者说:“如果阵列阵列设置为有挥发性的阵列,请写入挥发性变量之前的写入。我不清楚Hapens的原则。然后,我在线检查了相关信息,逐步学习,最终了解了JMM,然后在JMM下访问了JMM。方式。如果您不了解某个点,请在线查看信息或阅读相关书籍。由此,我也理解一个真理:简单地阅读书籍,这很容易疲倦和读取问题。每个人都会以您的心阅读,这更有利于加深个人理解。
总体而言,挥发性是并发编程中的优化,可以在某些情况下替换同步。但是,挥发性不能完全替换同步的位置。通常,只有在某些特殊情况下才能挥发。总的来说,锁定机制可以确保可见性和原子能,而挥发性变量可以确保可见性和刺激性的指示。因此,当变量的写入操作属于原子操作时,仅挥发性就可以使用它。我们可以。通用状态标记案例。责任指令,典型的是单个案例实现中的双检查锁。有兴趣的朋友可以阅读我之前写过的单模式文章的内容。