java代码在编译后会变成java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,java中所使用的的并发机制依赖与JVM的实现和CPU的指令。
1、volatile的应用
在多线程并发编程的中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,他在多处理器开发中保证了共享资源的“可见性”。如何volatile变量修饰符使用恰当的话,它比synchronized的使用执行成本更低,因为它不会引起线程的上下文切换和调度。
1.1 volatile的定义与实现原理
定义:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一直地更新,线程应该确保通过排他锁单独获得这个变量。java语言提供了volatile,在某些情况下闭锁更加方便。如果一个字段声明volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
在了解了volatile实现原理之前,我们先来看下与其实现原理相关的CPU术语与说明。下表是CPU术语的定义。
volatile是如何保证可见行的呢?让我们在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,cpu会做什么事情。
java代码如下:
转变成汇编代码,如下。
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock前缀指令在多核处理器下会引发两件事情。
- 将当前处理器缓存行的数据写回到系统内存
- 这个协会内存的操作会是在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
MESI协议缓存状态
1.2 volatile内存语义的实现
前文提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
综上所述:volatile的第二条语义:禁止指令重排序。
所以我们通常将volatile保证了可见性与有序性,但是不能保证多线程情况下无法保证原子性。
为什么volatile不能保证原子性?以i++为例,其包括读取、操作、赋值三个操作,下面是两个线程的操作顺序
假如说线程A在做了i+1,但未赋值的时候,线程B就开始读取i,那么当线程A赋值i=1,并回写到主内存,而此时线程B已经不再需要i的值了,而是直接交给处理器去做+1的操作,于是当线程B执行完并回写到主内存,i的值仍然是1,而不是预期的2。也就是说,volatile缩短了普通变量在不同线程之间执行的时间差,但仍然存有漏洞,依然不能保证原子性。总而言之volatile没有保证多线写操作的串行,所以不能保证原子性。
1.3 volatile的适用场景
- volatile是在synchronized性能低下的时候提出的。如今synchronized的效率已经大幅提升,所以volatile存在的意义不大。
- 如今非volatile的共享变量,在访问不是超级频繁的情况下,已经和volatile修饰的变量有同样的效果了。
- volatile不能保证原子性,这点是大家没太搞清楚的,所以很容易出错。
- volatile可以禁止重排序。
与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件 —— 即变量真正独立于其他变量和自己以前的值 —— 在某些情况下可以使用 volatile
代替 synchronized
来简化代码。然而,使用 volatile
的代码往往比使用锁的代码更加容易出错。
好了volatile就介绍到这里,以后在补充。