Java关键字之volatile

简介: Java volatile 关键字

1 前言

Java 编程语言允许线程访问共享变量。作为规则,为了确保共享变量被一致并可靠地更新,线程应该确保独占地使用这种变量,其惯用的方式是通过获取锁来实现,即强制线程互斥地使用这些变量。

Java 编程语言还提供了第二种机制,即 volatile,volatile 的意思是可见的,常用来修饰某个共享变量,意思是当共享变量的值被修改后,会及时通知其它线程,其它线程就能知道当前共享变量的值已经被修改了。在某些方面,它比加锁机制要方便。

字段可以被声明为 volatile ,此时 Java 内存模型会确保所有线程看到的都是该变量的一致的值。

如果 final 变量同时也被声明为 volatile ,那么就会产生一个编译时错误。

2 volatile 域

对于下面的例子:

publicclassNoSync {
staticinti=0, j=0;
staticvoidone() { i++; j++; }
staticvoidtwo() {
System.out.println("i="+i+" j="+j);
    }
publicstaticvoidmain(String[] args) {
newThread(() -> {
for (intk=0; k<10000; k++) {
one();
            }
        }).start();
newThread(() -> {
for (intk=0; k<10000; k++) {
two();
            }
        }).start();
    }
}

如果一个线程重复地调用方法 one(但是总共不超过 Integer.MAX_VALUE 次),而另一个线程重复地调用方法 two,那么方法 two 打印出的 j 的值偶尔会比 i 的值要大:

i=3315 j=3418
i=8350 j=8405
i=9107 j=9152
i=9715 j=9878
i=10000 j=10000
i=10000 j=10000
i=10000 j=10000

因为这个示例没有包含任何同步机制,共享变量 i 和 j 可能会被乱序更新。

—种可以防止这种乱序行为的方式是将方法 one 和 two 都声明为 synchronized 。

publicclassSync {
staticinti=0, j=0;
staticsynchronizedvoidone() { i++; j++; }
staticsynchronizedvoidtwo() {
System.out.println("i="+i+" j="+j);
    }
publicstaticvoidmain(String[] args) {
newThread(() -> {
for (intk=0; k<10000; k++) {
one();
            }
        }).start();
newThread(() -> {
for (intk=0; k<10000; k++) {
two();
            }
        }).start();
    }
}

这可以阻止方法 one 和方法 two 被并发地执行,并且可以确保共享变量 i 和 j 都会在方法 one 返回之前被更新。因此,方法 tow 永远都不会看到 j 的值大于 i 的值。实际上,它总是看到 i 和 j 有相同的值。

i=3738 j=3738
i=3738 j=3738
i=3738 j=3738
i=3738 j=3738
i=3738 j=3738
i=3919 j=3919
i=3922 j=3922
i=3922 j=3922
i=3922 j=3922
i=3922 j=3922
i=3922 j=3922
i=3922 j=3922

另一种方法是将 i 和 j 声明为 volatile:

publicclassVolatile {
staticvolatileinti=0, j=0;
staticvoidone() { i++; j++; }
staticvoidtwo() {
System.out.println("i="+i+" j="+j);
    }
publicstaticvoidmain(String[] args) {
newThread(() -> {
for (intk=0; k<10000; k++) {
one();
            }
        }).start();
newThread(() -> {
for (intk=0; k<10000; k++) {
two();
            }
        }).start();
    }
}

这使得方法 one 和方法 two 可以并发地执行,但是可以确保对共享变量 i 和 j 的访问发生的次数,与所有线程执行这段程序文本时这些访问出现的次数精确相等,并且以完全相同的顺序发生。因此,j 的共享值永远都不会大于 i 的共享值,因为每次对 i 的更新必须在 j 被更新之前反映到 i 的共享值中。但是,有可能会发现,任意给定的对方法 two 的调用都会观察到 j 的值比观察到的 i 的值大许多,因为方法 one 可能会在方法 two 抓取 i 的值的时刻与抓取 j 的值的时刻之间执行了许多次。

i=9260 j=9474
i=10000 j=10000
i=10000 j=10000
i=10000 j=10000
i=10000 j=10000
i=10000 j=10000
i=10000 j=10000

3 volatile的内存语义

在了解 volatile 实现原理之前,让我们先来了解一下与其实现原理相关的 CPU 相关的知识。现代计算机中,为了提高处理速度,处理器不直接和内存进行通信,而是会先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。这时候会产生这样一个问题,CPU 缓存中的值和内存中的值可能并不是时刻都同步的,导致线程获取到的用于计算的值可能不是最新的,共享变量的值有可能已经被其它线程所修改了,但此时修改的是机器内存的值,CPU 缓存的还是原来没有更新的值,从而就会导致计算出现问题。

那么 volatile 是如何来保证可见性的呢?

我们可以在 X86 处理器下通过工具获取 JIT 编译器生成的汇编指令来了解其背后的原理。

对于如下的 Java 代码:

instance=newSingleton(); // instance 是 volatile 变量

转变成的汇编代码如下:

0x01a3de1d: movb $0 * 0,0 * 1104800(%esi);
0x01a3de24: lock add1 $0 * 0,(%esp);

有 volatile 修饰的共享变量在进行写操作的时候会多出第二行汇编代码,通过查Intel IA-32 架构软件开发者手册的多处理器管理章节可知,Lock 前缀的指令在多核处理器下会引发两件事情:

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使在其他 CPU 里缓存里该内存地址的数据无效。

缓存行:CPU 高速缓存中可以分配的最小存储单位(通常为64个字节)。处理器填写缓存行时会加载整个缓存行,现代 CPU 需要执行几百次 CPU 指令。

如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令, 将这个变量所在的缓存行的数据写回到系统内存。但是,就算写回到内存,也不能保证其他处理器会立刻去内存中读取最新的值,这个时候处理器中缓存的值还是原来的旧值,线程在获取这个值执行计算操作时就会有问题。因此,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查字节缓存的值是不是过期了,如果发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。

volatile 的两条实现原则是:

  • Lock 前缀指令会引起处理器缓存写回到内存。 Lock 指令首先会尝试锁缓存,如果锁缓存无法保证独占共享内存,则会锁定整个总线。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

CPU每个缓存行标记四种状态(额外两位)

  • Modified:CPU修改了缓存中的数据(缓存中的数据与内存相比有更改过)
  • Exclusive:该缓存数据只由当前CPU使用(缓存中的数据是独享的)
  • Shared:缓存中的数据除了该CPU在读取,其他CPU也在读取
  • Invalid:缓存中的数据在读取时被其他CPU修改过

有些无法被缓存的数据(比较大的数据),或者跨越多个缓存行的数据依然必须使用总线锁。

volatile 变量读取过程.jpg

参考资料

目录
相关文章
|
3月前
|
存储 SQL 缓存
揭秘Java并发核心:深度剖析Java内存模型(JMM)与Volatile关键字的魔法底层,让你的多线程应用无懈可击
【8月更文挑战第4天】Java内存模型(JMM)是Java并发的核心,定义了多线程环境中变量的访问规则,确保原子性、可见性和有序性。JMM区分了主内存与工作内存,以提高性能但可能引入可见性问题。Volatile关键字确保变量的可见性和有序性,其作用于读写操作中插入内存屏障,避免缓存一致性问题。例如,在DCL单例模式中使用Volatile确保实例化过程的可见性。Volatile依赖内存屏障和缓存一致性协议,但不保证原子性,需与其他同步机制配合使用以构建安全的并发程序。
70 0
|
23天前
|
SQL 缓存 安全
[Java]volatile关键字
本文介绍了Java中volatile关键字的原理与应用,涵盖JMM规范、并发编程的三大特性(可见性、原子性、有序性),并通过示例详细解析了volatile如何实现可见性和有序性,以及如何结合synchronized、Lock和AtomicInteger确保原子性,最后讨论了volatile在单例模式中的经典应用。
28 0
|
2月前
|
缓存 Java 编译器
JAVA并发编程volatile核心原理
volatile是轻量级的并发解决方案,volatile修饰的变量,在多线程并发读写场景下,可以保证变量的可见性和有序性,具体是如何实现可见性和有序性。以及volatile缺点是什么?
|
3月前
|
安全 Java 编译器
Java 中的 volatile 变量
【8月更文挑战第22天】
27 4
|
3月前
|
缓存 安全 Java
Java里为什么单利一定要加volatile呢?
【8月更文挑战第11天】Java里为什么单利一定要加volatile呢?
29 3
|
3月前
|
缓存 安全 Java
Java里volatile底层是如何实现的?
【8月更文挑战第11天】Java里的volatile底层是如何实现的?
26 2
|
4月前
|
存储 SQL Java
Java实现关键字模糊查询的高效方法及实践
实现关键字模糊查询的方法有多种,每种方法都有其适用场景。在选择合适的方法时,应考虑实际需求、数据量大小、性能要求等因素。正则表达式适用于处理简单文本或小数据集;数据库模糊查询适用于存储在RDBMS中的数据;而第三方库,则适合需要进行复杂搜索的大型项目。选用合适的工具,可以有效提升搜索功能的性能和用户体验。
95 6
|
3月前
|
安全 Java
|
4月前
|
算法 Java API
多线程线程池问题之synchronized关键字在Java中的使用方法和底层实现,如何解决
多线程线程池问题之synchronized关键字在Java中的使用方法和底层实现,如何解决
|
3月前
|
缓存 Java 编译器
Java中的volatile 变量是什么
【8月更文挑战第10天】Java中的volatile 变量是什么
47 0