在多线程中,volatile和synchronized有着举足轻重的作用。其中volatile可以说是一种轻量级的synchroznized。
Java内存模型
下图简单介绍一下Java的内存模型。其中第一排表示不同线程A,B(B忘打了),第二排表示各自线程的私有缓存,第三排表示系统内存。为了提高处理速度,处理器不直接和内存通信,而是存到内部缓存中。
此时就出现了一个问题,内存不可见:一个变量X,在内存中是1,这是线程A和线程B同时取到了这个值,但是AX被修改为了2并写入到了系统内存中。此时线程B中X还是1,内存中X值是2。
这种情况可以使用volatile解决。
volatile定义
Java允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
从上面的定义可以看出来,如果一个变量被修饰为volatile,那么这个变量在所有的线程中都会看到一致的值。
volatile原理
volatile在实现依靠了底层cpu的指令,所以在学习volatile时需要先简单理解Java的实现原理。
Java运行原理:Java代码首先被编译 --> 转为Java字节码文件(.class)-->类加载器将字节码加载到JVM中--> JVM执行字节码转为汇编指令 --> 汇编指令在CPU上执行。
所以对于被volatile修饰了的变量来说,在进行写操作时,汇编代码会多出来一个lock指令,这个指令就是volatile实现并发的一个关键点。
volatile特性
众所周知,多线程有三大特性,原子性,有序性,可见性。其中volatile可以实现有序性和可见性。
- 可见性:
volatile可以保证不同线程对该变量进行修改之后,其他线程可以读取到修改之后的值。 - 有序性:
volatile通过禁止指令重排序从而保证有序性。 - 原子性:
volatile不能保证原子性。
volatile可见性原理
前文介绍到的lock指令对可见性起到了重要作用。
首先说一下lock的作用:
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效。
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,会将当前处理器的缓存行设置为不小状态,当处理器对这个数据进行操作的时候,会重新将系统内存的数据读取到处理器缓存中。
volatile有序性原理
内存屏障:一组处理器指令,用于实现对内存操作的顺序限制。
volatile会在生成字节码时插入内存屏障来禁止指令重排序。
volatile的优化
JDK7中增加了一个队列集合类Linked-TransferQueue,它在使用volatile时通过追加字节的方式来优化队列出队和入队的性能。一个对象的引用占4个字节,它追加了15个(共占60字节),加上父类的value的变量,一共64字节。
为什么64字节能提高并发编程的效率呢?
因为处理器L1,L2,L3缓存的高速缓存行是64字节宽,不支持部分填充缓存行,这意味着如果队列的头节点和尾节点都不足64字节,处理器会把他们读到一个缓存行中,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性的机制下,会导致其他处理器不能访问自己高速缓存行中的尾节点,而队列的入队和出队需要不同修改头节点和尾节点。所以在多处理器的情况下会严重影响效率。
下面两种情况不应该追加64字节。
- 缓存行非64字节宽的处理器。
- 共享变量不会被频繁地写。