Java语言规范对volatile
的定义
Java语言提供了volatile
,在某些情况下比锁更加方便,volatile
是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。Java语言规范对volatile的定义如下:
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
volatile
的实现原理
在了解volatile实现原理之前,需要先了解一下与其原理相关的CPU属于与说明。
术语 | 英文单词 | 描述 |
---|---|---|
内存屏障 | Memory Barriers | 是一组处理器指令,用于实现对内存操作的顺序限制。 |
缓冲行 | Cache line | 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期. |
原子操作 | Atomic operations | 不可中断的一个或一系列操作 |
缓存行填充 | cache line fill | 当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有) |
缓存命中 | Memory Barriers | 如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存。 |
写命中 | write hit | 当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称为写命中。 |
写缺失 | write misses the cache | 一个有效的缓存行被写入到不存在的内存区域。 |
那么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里缓存了该内存地址的数据无效。
一 .并发编程的三大特性
1. 原子性
原子性:即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
通过以下几个例子分析操作的原子性:
i = 0; //是原子操作。在Java中,对基本数据类型的变量和赋值操作都是原子性操作;
j = i ; //不是原子操作。包含了两个操作:读取i,将i值赋值给j
i++; //不是原子操作。包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;
i = j + 1; //不是原子操作。包含了三个操作:读取j值、j + 1 、将j+1的结果赋值给i;
在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的(注:在32位的JDK环境下,对64位数据的读取不是原子性操作,如long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized
来确保。volatile关键字不具备保证原子性的语义。
自JDK1.5版本起,其提供的原子类型变量也可以提供原子性。
2. 可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
Java提供了以下三种方式来保证可见性:
- 使用关键字
volatile
关键字。 - 通过
synchronized
关键字保证可见性。synchronized
关键字能保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在释放之前,会将变量的修改刷新到主内存中。 - 通过JUC提供的显示锁Lock保证可见性。
当一个变量被volatile修饰后,表示线程本地内存无效,当一个线程修改共享变量后它会立即被更新到主内存中,当其他线程读取共享变量时,它需要从主内存中再次读取。
3. 有序性
即程序执行的顺序按照代码的先后顺序执行。
一般来说,为了效率是允许编译器和处理器对指令进行优化,比如以下代码:
int x = 8;
int y = 0;
x++;
y = 15;
上面的代码定义了两个int类型的变量x和y,对x自增操作,对y进行赋值操作,从编写程序的角度来看,上面的代码肯定是顺序执行的,但是在JVM真正运行的时候就未必是这样的顺序了,比如y=15的语句可能会在x++语句前面得到执行,这种情况就是我们所说的指令重拍序。
Java提供volatile
关键字直接禁止JVM和处理器对其修饰的指令重排序,但是对于volatile
前后无依赖关系的指令则可以随便排序。
happens-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before原则中推到出来,那么他们就不能保证有序性,可以随意进行重排序。其定义如下:
- 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
- 监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
- 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
- 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
- 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
- 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)
二 .剖析volatile原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
通过翻阅OpenJDK下的unsafe.cpp源码可以发现,被volatile修饰的变量存在于一个“lock;”的前缀,其实际上相当于一个内存屏障,该内存屏障会为指令的执行提供如下几个保障:
- 确保指令重拍序时不会将其后面的代码排到内存屏障之前。
- 确保指令重拍序时不会将其前面的代码排到内存屏障之后。
- 确保在执行到内存屏障修饰的执行时其前面的指令代码全部执行完成。
- 强制将线程工作内存中的值修改刷新到主内存中。
- 如果是写操作,则会导致其他线程工作内存中的缓存数据失效。
volatile
的使用场景
- 开关控制利用可见性特点。定义一个
volatile
关键字修饰的变量,在多线程情况下根据其值执行各自逻辑 - 状态标记利用顺序性特点
- Singleton设计模式中的Double-check