引言
在多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized
,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
它在某些情况下比synchronized的开销更小,本文将深入分析在硬件层面上Inter处理器是如何实现Volatile的,通过深入分析能帮助我们正确的使用Volatile变量。
术语定义
术语
|
英文单词
|
描述
|
共享变量
|
|
在多个线程之间能够被共享的变量被称为共享变量。共享变量包括所有的实例变量,静态变量和数组元素。他们都被存放在堆内存中,Volatile只作用于共享变量。
|
内存屏障
|
Memory Barriers
|
是一组处理器指令,用于实现对内存操作的顺序限制。
|
缓冲行
|
Cache line
|
缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期。
|
原子操作
|
Atomic operations
|
不可中断的一个或一系列操作。
|
缓存行填充
|
cache line fill
|
当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有)
|
缓存命中
|
cache hit
|
如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存。
|
写命中
|
write hit
|
当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果不存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中。
|
写缺失
|
write misses the cache
|
一个有效的缓存行被写入到不存在的内存区域。
|
Volatile的官方定义
Java语言规范第三版中对volatile的定义如下:
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了
volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
为什么要使用Volatile
Volatile变量修饰符如果使用恰当
的话,它比synchronized的使用和执行成本会更低
,因为它不会引起线程上下文的切换和调度。
Volatile的实现原理
那么Volatile是如何来保证可见性的呢?在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。
Java代码:
|
instance = new Singleton();//instance是volatile变量
|
汇编代码:
|
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock
addl $0x0,(%esp);
|
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会
写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但
是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致
性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行
设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
这两件事情在IA-32软件开发者架构手册的第三册的多处理器管理章节(第八章)中有详细阐述。
Lock前缀指令会引起处理器缓存回写到内存
。Lock前缀指令导致在执行指令期间,声言处理器的 LOCK#
信号。在多处理器环境中,LOCK#
信号确保在声言该信号期间,处理器可以独占使用任何共享内存。(因为它会锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内
存),但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销比较大。在8.1.4章节有详细说明锁定操作对处理器缓存的影响,
对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和最近的处理器中,如果访问的内存区域已经缓存在处
理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁
定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据
。
一个处理器的缓存回写到内存会导致其他处理器的缓存无效
。IA-32处理器和Intel
64处理器使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32
和Intel
64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。例如
在Pentium和P6
family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在
下次访问相同内存地址时,强制执行缓存行填充。
Volatile的使用优化
著名的Java并发编程大师Doug lea在JDK7的并发包里新增一个队列集合类LinkedTransferQueue,他在使用Volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。
追加字节能优化性能?这种方式看起来很神奇,但如果深入理解处理器架构就能理解其中的奥秘。让我们先来看看LinkedTransferQueue
这个类,它使用一个内部类类型来定义队列的头队列(Head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类
AtomicReference只做了一件事情,就将共享变量追加到64字节。我们可以来计算下,一个对象的引用占4个字节,它追加了15个变量共占60
个字节,再加上父类的Value变量,一共64个字节。
/** head of the queue */
private transient final PaddedAtomicReference < QNode > head;
/** tail of the queue */
private transient final PaddedAtomicReference < QNode > tail;
static final class PaddedAtomicReference < T > extends AtomicReference < T > {
// enough padding for 64bytes with 4byte refs
Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
PaddedAtomicReference(T r) {
super(r);
}
}
public class AtomicReference < V > implements java.io.Serializable {
private volatile V value;
//省略其他代码 }
为什么追加64字节能够提高并发编程的效率呢
? 因为对于英特尔酷睿i7,酷睿, Atom和NetBurst, Core
Solo和Pentium
M处理器的L1,L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,这意味着如果队列的头节点和尾节点都不足64字节的话,处理器会将它
们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器试图修改头接点时会将整个缓存行锁定,那么在缓存一致性机制的
作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作是需要不停修改头接点和尾节点,所以在多处理器的情况下将会严重影响到
队列的入队和出队效率。Doug
lea使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头接点和尾节点加载到同一个缓存行,使得头尾节点在修改时不会互相锁定。
那么是不是在使用Volatile变量时都应该追加到64字节呢?不是的。在两种场景下不应该使用这种方式。第一:缓存行非64字节宽的处理器
,如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个字节宽。第二:共享变量不会被频繁的写
。因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,共享变量如果不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。
分享到:
相关推荐
1. **内存可见性**:当一个线程修改了Volatile变量的值,其他线程可以立即看到这个修改。这意味着Volatile变量在被修改后,无需额外的同步操作,其他线程就能获取到最新的值。 2. **禁止指令重排序**:编译器和...
### Java入门教程:数据类型与正确使用Volatile变量 #### 概述 在Java编程语言中,`volatile`关键字提供了一种轻量级的同步机制,用于确保共享变量的可见性和一定程度上的线程安全性。相比于传统的锁机制如`...
本资料《深入探讨Java多线程中的volatile变量》将带你深入理解这个概念,全面解析其工作原理和实际应用。 volatile关键字在Java中主要用于解决多线程环境下的可见性和有序性问题。它确保了被volatile修饰的变量对...
本文将深入分析在硬件层面上Inter处理器是如何实现Volatile的,通过深入分析能帮助我们正确的使用Volatile变量。
它确保了当一个线程修改了volatile变量的值时,其他线程可以立即看到这个新值,解决了Java内存模型导致的可见性问题。volatile的实现依赖于内存屏障,它确保在屏障前的写操作都会被写入内存,屏障后的读操作都能读取...
5. **原理**:在底层实现上,volatile变量在汇编代码中会包含一个内存屏障指令,如Intel x86架构下的`lock`前缀指令。这个指令有三个作用: - 阻止指令重排序 - 强制写入操作立即写入主内存 - 刷新其他处理器中...
x86处理器有L1、L2、L3缓存,以及多核之间的缓存一致性协议(如MESI协议),这些机制共同保证了多核环境下volatile变量的正确传播和一致性。 总结来说,volatile关键字通过禁止指令重排序和提供内存可见性,确保了...
在Java并发编程中,volatile关键字是一种轻量级的同步机制,它用于确保变量的可见性和有序性。本文将详细探讨volatile关键字的工作原理、使用场景以及如何在实际开发中正确使用volatile。 volatile关键字是Java并发...
- **未加 `volatile` 的示例**:在没有使用 `volatile` 修饰的情况下,其他线程可能因为缓存不一致而无法及时看到变量的变化。 - **加上 `volatile` 的示例**:通过使用 `volatile` 修饰符,可以确保其他线程能够...
但是这并不意味着对volatile变量的操作是线程安全的,因为有可能在读取到变量之后,又有其他线程对变量进行修改了。 例如,下面代码发起了20个线程,每个线程对race变量进行1万次自增操作。如果这段代码能够正确...
2. **volatile的实现原理** 在硬件层面,`volatile`关键字的实现主要依赖于处理器的内存模型和缓存一致性协议。在x86架构的处理器中,当一个`volatile`变量进行写操作时,会添加一个`lock`前缀的指令。这条指令做了...
这是因为`volatile`主要用于标记那些可能会被外部因素(如其他线程、中断服务程序等)意外改变的变量,从而确保程序能够正确地读取这些变量的最新值。 #### 二、volatile的工作原理 在默认情况下,编译器为了提高...
这是因为volatile变量的修改会立即刷新到主内存,而其他线程在访问时会从主内存中获取最新值,从而保证了数据的一致性。 2. 防止指令重排序:Java编译器和JVM为了优化性能,可能会对指令进行重排序。但是,对于...
Java中volatile关键字实现原理 volatile关键字是Java语言中的一种机制,用于保证变量在多线程之间的可见性。它是Java.util.concurrent包的核心,没有volatile就没有那么多的并发类供我们使用。本文详细解读一下...
首先,我们要明确volatile的两个主要特性:一是保证了共享变量的可见性,即当一个线程修改了volatile变量,其他线程可以立即看到修改;二是禁止指令重排序,防止数据的乱序读取,保证了单线程环境下代码的执行顺序。...
volatile关键字通过在汇编层面上添加lock指令,确保了对volatile变量的修改会被同步到主内存,并且其他线程在读取该变量时会从主内存中获取最新值,而不是使用自己的工作内存副本,从而解决了可见性问题。...
Linux 系统中断服务子程序的实现,包括了对中断服务子程序的定义、volatile 变量的使用、浮点运算的限制、printf 函数的使用限制等方面的知识点,旨在帮助读者更好地理解嵌入式 Linux 系统的工作原理。
#### 二、Volatile 的工作原理 - **内存模型**:Java 或 C++ 等语言都有自己的内存模型,`volatile` 关键字的实现依赖于这些内存模型。例如,在 Java 中,`volatile` 变量能够确保可见性,即当一个线程修改了 `...
本文将深入探讨其中的关键概念,包括读写锁、可重入锁、CAS原理以及volatile关键字。 首先,我们来看读写锁。读写锁允许多个线程同时进行读操作,但在写操作时,只有一个线程能够获得锁。这种设计极大地提高了并发...
Java中的`volatile`关键字是用来解决多线程环境下的可见性和有序性问题的,它确保了共享变量在被修改后,其他线程能够立即看到最新的值,但并不保证操作的原子性。下面我们将深入探讨`volatile`关键字的原理、使用...