最新要闻
- 环球关注:ChatGPT迅速学会下棋精髓 把人类给整不会了 网友:这谁预判得了?
- 世界快报:长城拼了!哈弗H6 DHT-PHEV限时优惠1.5万元:仅14.48万起
- 全球短讯!iPhone 15标准版上了灵动岛和USB-C接口:但跟Pro版差距还是太大
- 《狂飙》高启兰在日本火爆出圈 网友:《柯南》妃英理本理
- 灵活就业交医保太亏了怎么说?交医保太亏了具体指的是什么
- 苹果水下操控iPhone屏幕专利获批:或万元起售的Ultra机型独享
- 当前信息:魅族20电池寿命稳了:1600天连续充放容量仍大于80%
- 【新视野】日系车再受质疑!铃木维特拉遭车主集体投诉:刹车变硬踩不动
- 全球快讯:女子下班途中被狗撞伤被判工伤 法院:非本人主要责任
- AI创作享有版权吗?
- 世界视讯!卖的比“老头环”快!《霍格沃茨之遗》销量破1200万
- 今天是世界讨厌香菜日 不爱吃竟是“天注定”:跟基因有关
- 测试版用户终于能“反悔”了:Win11新功能允许退回正式版系统
- 今日报丨果然是“应试” IIHS碰撞测试难度提高:年度获奖车型数量腰斩
- 海绵宝宝是一种原始的什么动物?红色海绵球是干什么用的?
- 正者无敌三个太太的结局是什么?正者无敌演员表介绍
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
8000+字,就说一个字Volatile
简介
volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
Java volatile
关键字用于将Java变量标记为“存储在主存储器中”。更确切地说,这意味着,每次读取一个volatile变量都将从计算机的主内存中读取,而不是从CPU缓存中读取,并且每次写入volatile变量都将写入主内存,而不仅仅是CPU缓存。
实际上,自Java 5以来,volatile
关键字保证的不仅仅是向主存储器写入和读取volatile变量。我将在以下部分解释。
(资料图片)
特性
可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
COPYclass VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) { vl = l; //单个volatile变量的写 } public void getAndIncrement () { vl++; //复合(多个)volatile变量的读/写 } public long get() { return vl; //单个volatile变量的读 }}
假设有多个线程分别调用上面程序的三个方法,这个程序在语义上和下面程序等价:
COPYclass VolatileFeaturesExample { long vl = 0L; // 64位的long型普通变量 //对单个的普通 变量的写用同一个锁同步 public synchronized void set(long l) { vl = l; } public void getAndIncrement () { //普通方法调用 long temp = get(); //调用已同步的读方法 temp += 1L; //普通写操作 set(temp); //调用已同步的写方法 } public synchronized long get() { //对单个的普通变量的读用同一个锁同步 return vl; }}
如上面示例程序所示,对一个volatile变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同。
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有下列特性:
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
- 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
- 所有引用reference的赋值操作
- java.concurrent.Atomic.* 包中所有类的一切操作
可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
在线程使用非volatile变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主存储器拷贝到CPU高速缓存中。如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。这意味着,每个线程都可以将变量复制到不同CPU的CPU缓存中。这在这里说明:
对于volatile变量,无法保证Java虚拟机(JVM)何时将数据从主内存读取到CPU缓存中,或将数据从CPU缓存写入主内存。这可能会导致一些问题,我将在以下部分中解释。
想象一下两个或多个线程可以访问共享对象的情况,该共享对象包含一个声明如下的计数器变量:
COPYpublic class SharedObject { public int counter = 0;}
再想象一下,只有线程1对
counter
变量进行增加操作,但线程1和线程2都可能读取变量counter
。
如果counter
变量未声明volatile
,则无法保证何时将counter
变量的值从CPU缓存写回主存储器。这意味着,CPU高速缓存中的counter
变量值可能与主存储器中的变量值不同。这种情况如下所示:
线程没有看到变量的最新值的问题,是因为它还没有被另一个线程写回主内存,这被称为“可见性”问题,其他线程看不到一个线程的某些更新。
volatile可见性保证
Java
volatile
关键字旨在解决变量可见性问题。通过使用volatile
声明counter
变量,对变量counter
的所有写操作都将立即写回主存储器。此外,counter
变量的所有读取都将直接从主存储器中读取。
下面是counter
变量声明为volatile
的样子:
COPYpublic class SharedObject { public volatile int counter = 0;}
声明变量为
volatile
,对其他线程写入该变量 保证了可见性。
在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明该counter
变量为volatile
足以保证写入counter
变量对T2的可见性。
但是,如果T1和T2都在增加counter
变量,那么声明counter
变量为volatile
就不够了。稍后会详细介绍。
完全volatile可见性保证
实际上,Java
volatile
的可见性保证超出了volatile
变量本身。可见性保证如下:
- 如果线程A写入
volatile
变量并且线程B随后读取这个volatile
变量,则在写入volatile
变量之前对线程A可见的所有变量在线程B读取volatile
变量后也将对线程B可见。 - 如果线程A读取
volatile
变量,则读取volatile
变量时对线程A可见的所有变量也将从主存储器重新读取。
让我用代码示例说明:
COPYpublic class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}
udpate()
方法写入三个变量,其中只有days
是volatile变量。
完全volatile
可见性保证意味着,当将一个值写入days
时,对线程可见的其他所有变量也会写入主存储器。这意味着,当一个值被写入days
,years
和months
的值也被写入主存储器(注意days的写入在最后)。
当读取
years
,months
和days
的值你可以这样做:
COPYpublic class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}
注意totalDays()
方法通过读取days
的值到total变量中开始。当读取days
的值时,后续months
和years
值的读取也会从主存储器中读取。因此使用上述读取序列可以保证看到最新的days
,months
和years
值。
有序性
即程序执行的顺序按照代码的先后顺序执行。
java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。 在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
volatile变量的特性
保证可见性,不保证原子性
- 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
- 这个写会操作会导致其他线程中的缓存无效。
禁止指令重排
重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运
行时这两个操作不会被重排序。
重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发
生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
指令重排序
出于性能原因允许JVM和CPU重新排序程序中的指令,只要指令的语义含义保持不变即可。例如,查看下面的指令:
COPYint a = 1;int b = 2;a++;b++;
这些指令可以按以下顺序重新排序,而不会丢失程序的语义含义:
COPYint a = 1;a++;int b = 2;b++;
然而,当其中一个变量是volatile
变量时,指令重排序会出现一个挑战。让我们看看MyClass
这个前面Java volatile教程中的例子中出现的类:
COPYpublic class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}
一旦update()
方法写入一个值days
,新写入的值,以years
和months
也被写入主存储器。但是,如果JVM重新排序指令,如下所示:
COPYpublic void update(int years, int months, int days){ this.days = days; this.months = months; this.years = years;}
当days
变量被修改时months
和years
的值仍然写入主内存中,但是这一次它发生在新的值被写入months
和years
之前,也就是这两个变量的旧值会写入主存中,后面两句的写入操作只是写到缓存中。因此,新值不能正确地对其他线程可见。重新排序的指令的语义含义已经改变。
happens before
上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注。
从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。
从内存语义的角度来说,volatile与锁有相同的效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。
请看下面使用volatile变量的示例代码:
COPYclass VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; //1 flag = true; //2 } public void reader() { if (flag) { //3 int i = a; //4 …… } }}
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:
- 根据程序次序规则,1 happens before 2; 3 happens before 4。
- 根据volatile规则,2 happens before 3。
- 根据happens before 的传递性规则,1 happens before 4。
上述happens before 关系的图形化表现形式如下:
上图中,每一个箭头链接的两个节点,代表了一个happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens before保证。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
Happens-Before 保证
为了解决指令重排序挑战,除了可见性保证之外,Java
volatile
关键字还提供“happens-before”保证。happens-before保证保证:
volatile 之前读写
如果读取/写入最初发生在写入volatile
变量之前,读取/写入其他变量不能重新排序在写入volatile
变量之后。 写入volatile
变量之前的读/写操作被保证 “happen before” 写入volatile
变量。请注意,发生在写入volatile
变量之后的读/写操作依然可以重排序到写入volatile
变量前,只是不能相反。允许从后到前,但不允许从前到后。
volatile 之后读写
如果读/写操作最初发生在读取volatile
变量之后,则读取/写入其他变量不能重排序到发生在读取volatile
变量之前。请注意,发生在读取volatile
变量之前的读/写操作依然可以重排序到读取volatile
变量后,只是不能相反。允许从前到后,但不允许从后到前。
上述 “happens-before”规则保证确保volatile
关键字的可见性保证在强制执行。
COPYpublic class VolatileTest { private volatile int vi = 1; private int i = 2; private int i2 = 3; @Test public void test() { System.out.println(i); //1 读取普通变量 i=3; //2 写入普通变量 //1 2 不能重排序到3之后,操作4可以重排序到3前面 vi = 2; //3 写入volatile变量 i2 = 5; //4 写入普通变量 } @Test public void test2() { System.out.println(i); //1 读取普通变量 //3不能重排序到在2前,但1可以重排序到2后 System.out.println(vi); //2 读取volatile变量 System.out.println(i2); //3 读取普通变量 }}
volatile注意事项
volatile 线程不安全
即使
volatile
关键字保证volatile
变量的所有读取直接从主存储器读取,并且所有对volatile
变量的写入都直接写入主存储器,仍然存在声明volatile
变量线程不安全。
在前面解释的情况中,只有线程1写入共享counter
变量,声明counter
变量为volatile
足以确保线程2始终看到最新的写入值。
实际上,如果写入volatile
变量的新值不依赖于其先前的值,则甚至可以多个线程写入共享变量,并且仍然可以在主存储器中存储正确的值。换句话说,就是将值写入共享volatile
变量的线程开始并不需要读取其旧值来计算其下一个值。
一旦线程需要首先读取volatile
变量的旧值,并且基于该值为共享volatile
变量生成新值,volatile
变量就不再足以保证正确的可见性。读取volatile
变量和写入新值之间的短时间间隔会产生竞争条件 ,其中多个线程可能读取volatile
变量的同一个旧值,然后为其生成新值,并将该值写回主内存 - 覆盖彼此的值。
多个线程递增同一个计数器的情况正是 volatile
变量并不安全的情况。以下部分更详细地解释了这种情况。
想象一下,如果线程1将值为0的共享变量counter
读入其CPU高速缓存,将其增加到1并且不将更改的值写回主存储器。然后,线程2也从主存储器读取相同的counter
变量进入自己的CPU高速缓存,其中变量的值仍为0。然后,线程2也将计数器递增到1,也不将其写回主存储器。这种情况如下图所示:
线程1和线程2现在失去了同步。共享变量counter
的实际值应为2,但每个线程的CPU缓存中的变量值为1,而在主内存中,该值仍为0。这是一个混乱!即使线程最终将共享变量counter
的值写回主存储器,该值也将是错误的。
保证线程安全
正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用 volatile
关键字是不安全的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子性的。读取或写入一个volatile变量不会阻塞其他线程读取或写入这个变量。为此,您必须在临界区周围使用synchronized
关键字。
作为synchronized
块的替代方法,您还可以使用java.util.concurrent
包中众多的原子数据类型。例如,AtomicLong
或者 AtomicReference
或其他的。
如果只有一个线程读取和写入volatile变量的值,而其他线程只读取这个变量,那么此线程将保证其他线程能看到volatile变量的最新值。如果不将变量声明为volatile
,则无法保证。
volatile
关键字也可以保证在64位变量上正常使用。
volatile的性能考虑
读取和写入volatile变量会导致变量从主存中读取或写入主存,读取和写入主内存比访问CPU缓存开销更大。访问volatile变量也会阻止指令重排序,这是一种正常的性能提升技术。因此,当您确实需要强制实施变量可见性时,才使用volatile变量。
原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
内存语义
volatile写的内存语义
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:
如上图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。
volatile读的内存语义
当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
下面是线程 B 读同一个 volatile 变量后,共享变量的状态示意图:
如上图所示,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。
如果我们把volatile写和volatile读这两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。
小结
下面对volatile写和volatile读的内存语义做个总结
- 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
- 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
volatile内存语义的实现
前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:
是否能重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从上表我们可以看出
- 当第二个操作为volatile写操作时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile写之后;
- 当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile读之前;
- 当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。
为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。
- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)。
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)。
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)。
其中重点说下StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。上述内存屏障插入策略非常保守,但它可以保证在任意处理平台,任意的程序中都能得到正确的volatile语义。下面是保守策略(为什么说保守呢,因为有些在实际的场景是可省略的)下,volatile 写操作 插入内存屏障后生成的指令序列示意图:
其中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作对任意处理器可见(把它刷新到主内存)。
另外volatile写后面有StoreLoad屏障,此屏障的作用是避免volatile写与后面可能有的读或写操作进行重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)为了保证能正确实现volatile的内存语义,JMM采取了保守策略:在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见模式是:一个写线程写volatile变量,多个度线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里也可看出JMM在实现上的一个特点:首先确保正确性,然后再去追求效率(其实我们工作中编码也是一样)。
下面是在保守策略下,volatile读插入内存屏障后生产的指令序列示意图:
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况忽略不必要的屏障。在JMM基础中就有提到过各个处理器对各个屏障的支持度,其中x86处理器仅会对写-读操作做重排序。
下面我们通过具体的示例代码来说明
COPYclass VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1; //第一个volatile读 int j = v2; // 第二个volatile读 a = i + j; //普通写 v1 = i + 1; // 第一个volatile写 v2 = j * 2; //第二个 volatile写 } … //其他方法}
针对 readAndWrite() 方法,编译器在生成字节码时可以做如下的优化:
注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。
上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。
前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:
前文提到过,x86 处理器仅会对写 - 读操作做重排序。,x86处理器仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。
为什么要增强volatile的内存语义
在 JSR-133 之前的旧 Java 内存模型中,虽然不允许 volatile 变量之间重排序,但旧的 Java 内存模型允许 volatile 变量与普通变量之间重排序。在旧的内存模型中,VolatileExample 示例程序可能被重排序成下列时序来执行:
在旧的内存模型中,当 1 和 2 之间没有数据依赖关系时,1 和 2 之间就可能被重排序(3 和 4 类似)。其结果就是:读线程 B 执行 4 时,不一定能看到写线程 A 在执行 1 时对共享变量的修改。
因此在旧的内存模型中 ,volatile的写-读没有锁的释放-获所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。
由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。如果读者想在程序中用volatile代替监视器锁,请一定谨慎,具体细节请参阅参考Java理论与实践:正确使用Volatile变量。
本文由
传智教育博学谷狂野架构师
教研团队发布。如果本文对您有帮助,欢迎
关注
和点赞
;如果您有任何建议也可留言评论
或私信
,您的支持是我坚持创作的动力。转载请注明出处!
8000+字,就说一个字Volatile
环球关注:ChatGPT迅速学会下棋精髓 把人类给整不会了 网友:这谁预判得了?
世界快报:长城拼了!哈弗H6 DHT-PHEV限时优惠1.5万元:仅14.48万起
全球短讯!iPhone 15标准版上了灵动岛和USB-C接口:但跟Pro版差距还是太大
《狂飙》高启兰在日本火爆出圈 网友:《柯南》妃英理本理
全球今亮点!数字孪生|可视化图表之分组柱状图
世界今日讯!测试角色在项目各阶段的项目管理tips
当前速读:【JVM】JDK7后intern方法总结
世界热讯:centos7多网口配置同网段IP解决方案
焦点快播:【Vue】vue3 元素在某区域内缩放拖拽
灵活就业交医保太亏了怎么说?交医保太亏了具体指的是什么
苹果水下操控iPhone屏幕专利获批:或万元起售的Ultra机型独享
当前信息:魅族20电池寿命稳了:1600天连续充放容量仍大于80%
【新视野】日系车再受质疑!铃木维特拉遭车主集体投诉:刹车变硬踩不动
全球快讯:女子下班途中被狗撞伤被判工伤 法院:非本人主要责任
LGV 引理
【报资讯】如何实现把多个git仓库合并为一个,并保留提交记录?
AI创作享有版权吗?
世界视讯!卖的比“老头环”快!《霍格沃茨之遗》销量破1200万
今天是世界讨厌香菜日 不爱吃竟是“天注定”:跟基因有关
测试版用户终于能“反悔”了:Win11新功能允许退回正式版系统
今日报丨果然是“应试” IIHS碰撞测试难度提高:年度获奖车型数量腰斩
海绵宝宝是一种原始的什么动物?红色海绵球是干什么用的?
正者无敌三个太太的结局是什么?正者无敌演员表介绍
宫锁连城为什么下架了?宫锁连城的大结局是什么?
安全帽能代替头盔吗?安全帽颜色的级别区分
光盘怎么进行拷贝?光盘拷贝到u盘需要多少钱?
word安全模式是怎么回事?word安全模式怎么解除?
苹果怎么查询激活时间?苹果怎么传输数据到新的手机上?
闪存卡损坏是什么原因?闪存卡损坏怎样修复?
当前速看:易基因|DNA甲基化研究的测序数据挖掘思路:干货分享
速看:Centos7单机部署Etcd
Springcloud~openfeign开启hystrix基于线程池熔断的传值问题
环球热点!产品经理
【网关开发】9.Openresty 自定义流量分流策略支持灰度(金丝雀)等发布业务场景
每日热议!刘洋一杆领跑资格考试36洞 四人并列第二
天天热议:曾凭五菱宏光MINI EV火爆出圈 小米汽车营销负责人周钘离职
全球限量1962台!徕卡推出D-Lux7 “007”限量版相机:售价达16800元
全球最资讯丨《嗜血印》魅魔DLC完善更新 新增魅魔纹和性感尾巴
全球热点!抄底手慢无:南国生椰拿铁33.9元起32杯(赠冰川杯)
热推荐:不满足于对话!微软希望ChatGPT控制机器为人服务
网络时间同步设备(时钟同步)产品的功能及技术参数
每日简讯:MegEngine 使用小技巧:使用 Netron 实现模型可视化
环球今日讯!切换页签,再切换回来,v-tooltip会一直显示问题
世界即时看!小米智能工厂二期项目主体结构已封顶:年底竣工交付
全球热消息:小米汽车明年量产!雷军晒新到手礼物:F1传奇设计师自传
世界百事通!博主实探上海巴奴火锅店:土豆11元6片 换算下来45元1斤
加密劫持病毒现身苹果macOS:盗版软件成主要途径
努比亚Z50 Ultra官宣:真全面屏!
全球速递!巴克利:湖人把一切归咎于威少会让他愤怒 在快船有最佳夺冠机会
全球今亮点!《霍格沃茨之遗》温咖癫啦维欧萨
最快2025年商用!日本飞行汽车载人试飞成功:机体中国造
天天最资讯丨记Cucumber行为驱动测试的简单配置与使用方式
老人推倒摩托后去世 继承人被起诉背后:老人是惯犯 车主不能忍
【全球独家】读Java实战(第二版)笔记19_尾声
全球短讯!衡水新型冠状病毒肺炎疫情:2月24日衡水疫情最新消息今天数据统计情况通报
全球即时:搜狗输入法UOS版上架统信UOS:适配龙芯LoongArch等架构
每日短讯:干翻 nio ,王炸 io_uring 来了 !!(图解+史上最全)
全球快看:CPU推理|使用英特尔 Sapphire Rapids 加速 PyTorch Transformers
世界视点!死锁面试题
18、实体类对象比对-JSON
产品经理,项目经理,FTO
当前最新:《狂飙》取景地拍照收费摊主已搬离:没给续签合同
环球速看:索尼A7M4将发布2.0固件 或下放部分索尼A7R5功能
80%的人出错了 你的数码相机用对了吗?
全球热讯:爱奇艺奇迹一般的赚钱了!但是 就这它也好意思?
快看点丨AMD Zen4锐龙9 7945HX大放异彩!16核心打平Intel 24核心
世界今头条!长色斑的原因有哪些_脸上为什么会长色斑
热门:007 - 研究
每日信息:47.多态
第二章 物理层
day02-自己实现Mybatis底层机制-01
世界快资讯:CSS背景设置与Emmet语法
环球观焦点:AMD发布23.2.2版驱动:RX 7900显卡小打鸡血 性能提升14%
天天最新:大理州5个新能源装备制造项目投产
深圳一外卖小哥疑送餐时猝死:曾拼命跑上六楼
环球热头条丨向上捅破天 吉利银河支持低轨卫星技术:全球无盲区定位
13代酷睿i9+满血4060显卡!华硕天选4正式开售 到手价8999元
当前短讯!造车新势力 电动自行车品牌“VELOTRIC”A轮融资:获5000万元
中疾控提醒:近期水痘处于高发期 要注意做好防护!
13代酷睿+RTX 4060!七彩虹将星X16 Pro图赏
19999元起 雷蛇推出2023款灵刃15游戏本:i7+RTX 4060
股价大涨7% 阿里发布Q3财报:营收2477.6亿 利润超456亿
全球快报:第八章 从源文件到可执行文件
天天通讯!《黑豹》《蚁人》皆扑街 漫威超级英雄为何在国内失宠?
男子一脚急刹车醒来直接四肢瘫痪 医生提醒:车祸受伤后别随意拖拽
焦点快看:系列首次支持:三星Galaxy Tab S9平板终于支持IP67防水
每日热文:三星S23首销:仅重168g的骁龙8 Gen2旗舰 屏幕比小米13更小
涨价2000只是开胃菜!销售:特斯拉还要涨价
全球今热点:电工化身“Tony”,为留守老人解决“头”等大事
环球聚焦:邓超谈《中国乒乓》排片少:大家的不容易我非常理解
今亮点!摩根大通已限制员工使用ChatGPT:数据安全更重要
世界视讯!我国第三款国产ECMO产品获批上市:性能达到国际水平
全球即时:月薪4万招人去非洲养鸡当场长?企业回应:环境艰苦 不招年轻人
当前焦点!Windows 上 Docker 部署 MongoDb 并构建数据持久化
2022最新整理iOS app上架app详细教程
webrtc QOS笔记二 音频buffer数据不足生成很多gap的问题
当前最新:记录--前端项目中运行 npm run xxx 的时候发生了什么?
前沿资讯!科考中的意外收获!中国科学家在非洲发现消失百余年的濒危植物
天天速读:极氪001再遭奇葩故障:中控黑了、仪表花了、HUD“涂马赛克”