java volatile 使用及其原理B java层面

本文深入探讨Java中volatile关键字如何确保多线程环境下的内存可见性。通过分析JMM内存模型,指令重排序规则,以及volatile内存语义的实现机制,揭示volatile变量读写操作的特性,包括可见性和原子性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

volatile 在java中是怎样保证可见性的

 

本文摘自《java并发编程的艺术》一书

1、对JMM有了解

2、清楚指令重排序(可参考《java并发编程的艺术》一书)

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类
型。
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图3-3所示。

上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序
出现内存可见性问题。对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排
序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要
求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为
Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁
止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

用volatile关键字是因为有内存屏障所以能保证可见性,那么内存屏障具体是什么样子的?

首先了解一下有哪些屏障

 

happens-before原则

volatile的内存语义
当声明共享变量为volatile后,对这个变量的读/写将会很特别。为了揭开volatile的神秘面
纱,下面将介绍volatile的内存语义及volatile内存语义的实现。

3.4.1 volatile的特性
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这
些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量
public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement () {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class 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变量自身具有下列特性。
·可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写
入。
·原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不
具有原子性。

 volatile内存语义的实现
下面来看看JMM如何实现volatile写/读的内存语义。
前文提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM
会分别限制这两种类型的重排序类型。表3-5是JMM针对编译器制定的volatile重排序规则表。
表3-5 volatile重排序规则表

 

 

 


举例来说,第三行最后一个单元格的意思是:在程序中,当第一个操作为普通变量的读或
写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从表3-5我们可以看出。
·当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保
volatile写之前的操作不会被编译器重排序到volatile写之后。
·当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保
volatile读之后的操作不会被编译器重排序到volatile读之前。
·当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来
禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总
数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
·在每个volatile写操作的前面插入一个StoreStore屏障。
·在每个volatile写操作的后面插入一个StoreLoad屏障。
·在每个volatile读操作的后面插入一个LoadLoad屏障。
·在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能
得到正确的volatile内存语义。

 

下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图,如图3-19所示。

 

图3-19中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任
意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主
内存。
这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与
后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面
是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确
实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile
读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM最终选择了在每个
volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个
写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,
选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM
在实现上的一个特点:首先确保正确性,然后再去追求执行效率

 

 

图3-20中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变
volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面通过具体的示例
代码进行说明。
class 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处理器为例,图3-21
中除最后的StoreLoad屏障外,其他的屏障都会被省略。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值