volatile关键字理解

volatile关键字有以下几大特性:

  1. 保证内存 可见性
  2. 防止指令 重排序
  3. 保证对 64 位变量 读写的原子性
  4. 不能保证线程安全

保证内存可见性

什么情况下需要保证内存可见性?

在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下,volatile关键字的使用变得非常重要。

在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。使用volatile修饰这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。

注意:

在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

volatile做了什么?

对于 volatile 修饰的变量,JVM 可以保证:

  1. 每次对该变量的写操作,都将立即同步到主存;
  2. 每次对该变量的读操作,都将从主存读取,而不是线程栈

防止指令重排序

如果一个操作不是原子操作,那么 JVM 便可能会对该操作涉及的指令进行重排序。重排序需要按照as-if-serial语义的规则,通过调整指令的执行顺序,尽可能达到提高运行效率的目的。

as-if-serial语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。所以编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

在 JDK 1.5 之后,增强了 volatile 的语义,严格限制 JVM (编译器、处理器)不能对 volatile 修饰的变量涉及的操作指令进行重排序。

保证对 64 位变量 读写的原子性

JVM 可以保证对 32位 数据读写的原子性,但是对于 long 和 double 这样 64位 的数据的读写,会将其分为 高32位 和 低32位 分两次读写。所以对于long 或 double 的读写并不是原子性的,这样在并发程序中共享 long 或 double 变量就可能会出现问题,于是 JVM 提供了 volatile 关键字来解决这个问题:使用 volatile 修饰的 long 或 double 变量,JVM 可以保证对其读写的原子性。但值得注意的是,此处的 “写” 仅指对 64位 的变量进行直接赋值。

不能保证线程安全

volatile只能保证被其修饰变量的内存可见性,但如果对该变量执行的是非原子操作线程依旧是不安全的。而对于 i++ 这个语句,事实上涉及了 读取-修改-写入 三个操作:

  1. 读取变量到栈中某个位置
  2. 对栈中该位置的值进行自增
  3. 将自增后的值写回到变量对应的存储位置

因此哪怕变量 i 使用 volatile 修饰,也并不能使涉及上面三个操作的 i++ 具有原子性。所以多线程条件下使用 volatile 关键字的前提是:对变量的写操作不依赖于变量的当前值,而赋值操作很明显满足这一前提。

总结

在多线程环境下,正确使用 volatile 关键字可以比直接使用 synchronized 更加高效而且代码简洁,但是使用 volatile 关键字也更容易出错。所以,除非十分清楚 volatile 的使用场景,否则还是应该选择更加具有保障性的 synchronized

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值