Java的volatile到底该如何理解?(下)

简介: Java的volatile到底该如何理解?(下)

对于Long和double型变量的特殊规则

虚拟机规范中,写64位的double和long分成了两次32位值的操作

由于不是原子操作,可能导致读取到某次写操作中64位的前32位,以及另外一次写操作的后32位


读写volatile的long和double总是原子的。读写引用也总是原子的


商业JVM不会存在这个问题,虽然规范没要求实现原子性,但是考虑到实际应用,大部分都实现了原子性。

对于32位平台,64位的操作需要分两步来进行,与主存的同步。所以可能出现“半个变量”的状态。

21.png

在实际开发中,目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此我们在编码时一般不需要把用到的long和double变量专门声明为volatile。

Word Tearing字节处理

一个字段或元素的更新不得与任何其他字段或元素的读取或更新交互。

特别是,分别更新字节数组的相邻元素的两个线程不得干涉或交互,也不需要同步以确保顺序一致性。


有些处理器(尤其是早期的Alphas处理器)没有提供写单个字节的功能。

在这样的处理器_上更新byte数组,若只是简单地读取整个内容,更新对应的字节,然后将整个内容再写回内存,将是不合法的。


这个问题有时候被称为“字分裂(word tearing)”,在单独更新单个字节有难度的处理器上,就需要寻求其它方式了。

基本不需要考虑这个,了解就好。

JAVA代码层级 - volatile
JVM层级 - JSR
os - 具体实现

内存屏障

JSR的内存屏障(JVM 规范)

这只是 JVM 层级的要求,非底层硬件的具体实现!

  • 在 volatile 读写前后都加上屏障
  • 22.png

LoadLoad屏障

对于这样的语句Load1; Loadload; Load2,

在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕

StoreStore屏障

对于这样的语句Store1; StoreStore; Store2,

在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障

对于这样的语甸oad1; LoadStore; Store2,

在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

Storeload屏障

对于这样的语句Store1; StoreL oad; Ioad2,

在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

JVM 层面 volatile的实现细节

23.png

x86 CPU 内存屏障 - 原语级别实现

之所以JVM不直接使用这些指令,是因为并非所有 cpu 都支持,但是所有 cpu 都支持 lock 指令!

lock 指令直接锁定总线,肯定直接禁止了重排序,因此 JVM是调用了该指令,简单暴力!

24.png

  • sfence
    在sfence指令前的写操作当必须在sfence指令后的写操作前完成
  • lfence
    在Ifence指令前的读操作当必须在Ifence指令后的读操作前完成
  • mfence
    在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
  • 有序性保障:intel lock 汇编指令

原子指令,如x86上的lock指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序


处理器提供了两个内存屏障指令(Memory Barrier)用于解决上述的两个问题:

7.1 指令分类

  • 写内存屏障(Store Memory Barrier)
    在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见

强制写入主内存,这种显示调用,CPU就不会因为性能考虑而去对指令重排


读内存屏障(Load Memory Barrier)

在指令前插入Load Barrier,可以让高速缓存中的数

据失效,强制从新从主内存加载数据。

强制读取主内存内容,让CPU缓存与主内存保持一致,避免了缓存导致的一致性问题

7.2 有序性(Ordering)

JMM中程序的天然有序性可以总结为一句话:

如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。

前半句是指“线程内表现为串行语义”

后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象


Java提供了volatile和synchronized保证线程之间操作的有序性

volatile本身就包含了禁止指令重排序的语义

synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

7.3 Happens-beofre 先行发生原则(JVM 规范)

这八种不能指令重排序

如果JMM中所有的有序性都只靠volatile和synchronized,那么有一些操作将会变得很繁琐,但我们在编写Java并发代码时并没有感到这一点,这是因为Java语言中有一个先行发生(Happen-Before)原则。它是判断数据是否存在竞争,线程是否安全的主要依赖。


先行发生原则

JMM中定义的两项操作之间的依序关系。

happens- before关系 主要是强调两个有冲突的动作之间的顺序,以及定义数据争用的发生时机。


如果操作A先行发生于操作B,就是在说发生B前,A产生的影响能被B观察到,“影响”包含了修改内存中共享变量的值、发送了消息、调用了方法等。案例如下:

// 线程A中执行  
i = 1;  
// 线程B中执行  
j = i;  
// 线程C中执行  
i = 2;

下面是JMM下一些”天然的“先行发生关系,无须任何同步器协助就已经存在,可以在编码中直接使用。

如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意重排序。


具体的虚拟机实现,有必要确保以下原则的成立:


程序次序规则(Pragram Order Rule)

在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。

对象锁(监视器锁)法则(Monitor Lock Rule )

某个管程(也叫做对象锁,监视器锁) 上的unlock动作happens-before同一个管程上后续的lock动作 。这里必须强调的是同一个锁,而”后面“是指时间上的先后。

volatile变量规则(Volatile Variable Rule)

对某个volatile字段的写操作happens- before每个后续对该volatile字段的读操作,这里的”后面“同样指时间上的先后顺序。

线程启动规则(Thread Start Rule)

在某个线程对象 上调用start()方法happens- before该启动了的线程中的任意动作

线程终止规则(Thread Termination Rule)

某线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束(任意其它线程成功从该线程对象上的join()中返回),Thread.isAlive()的返回值等作段检测到线程已经终止执行。

线程中断规则(Thread Interruption Rule)

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生

对象终结规则(Finalizer Rule)

一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始

传递性(Transitivity)

如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的,一个典型的例子就是指令重排序。

所以时间上的先后顺序与先行发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。

7.4 作用

1.阻止屏障两侧的指令重排序
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效


对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据

对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见


Java的内存屏障实际上也是上述两种的组合,完成一系列的屏障和数据同步功能LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障: 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障: 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile的内存屏障策略非常严格保守


在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障

在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障


由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。


总结

看到了现代CPU不断演进,在程序运行优化中做出的努力。不同CPU厂商所付出的人力物力成本,最终体现在不同CPU性能差距上。而Java就随即推出了大量保证线程安全的机




目录
相关文章
|
8月前
|
缓存 人工智能 安全
4. Java 的线程安全机制之`volatile`
4. Java 的线程安全机制之`volatile`
|
8月前
|
存储 Java
「Java面试」工作3年竟然回答不出如何理解Reentrantlock实现原理
一个3 年工作经验的小伙伴,在面试的时候被这样一个问题。”谈谈你对ReentrantLock实现原理的理解“,他当时零零散散的说了一些。但好像没有说关键点。希望我分享一下我的理解。
69 0
|
4天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
26 0
|
4天前
|
存储 安全 Java
[Java]volatile关键字
[Java]volatile关键字
31 0
|
4天前
|
存储 缓存 Java
Java volatile关键字-单例模式的双重锁为什么要加volatile
Java volatile关键字--单例模式的双重锁为什么要加volatile
54 10
|
4天前
|
Java 编译器 程序员
Volatile:Java并发编程的隐形英雄
Volatile:Java并发编程的隐形英雄
26 0
|
4天前
|
缓存 人工智能 安全
4. Java 的线程安全机制之`volatile`
4. Java 的线程安全机制之`volatile`
|
4天前
|
Java 编译器
Java多线程:什么是volatile关键字?
Java多线程:什么是volatile关键字?
32 0
|
4天前
|
SQL 缓存 安全
java中volatile关键字
java中volatile关键字
42 0
java中volatile关键字
|
4天前
|
Java
多线程与并发编程:解释什么是死锁,并给出一个在Java中发生死锁的例子。描述一下Java中的volatile关键字的作用,以及它与synchronized的区别。
多线程与并发编程:解释什么是死锁,并给出一个在Java中发生死锁的例子。描述一下Java中的volatile关键字的作用,以及它与synchronized的区别。
29 0