volatile作用
volatile
主要是为了解决多线程内存不可见问题。
- 对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
其次是为了保证代码的有序性。
虚拟机在编译的时候,是有可能把代码的顺序进行重排序的,不一定会按照我们写的代码的顺序来执行,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题的。
- 如果一个变量被声明
volatile
的话,那么这个变量不会被进行重排序,也就是说,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它慢执行。
总结一下volatile
作用主要就是两点:
- 可见性
- 有序性
内存不可见原因
- cache机制导致内存不可见
- CPU的运行速度是远远高于内存的读写速度的,为了不让cpu为了等待读写内存数据,现代cpu和内存之间都存在一个高速缓存cache(实际上是一个多级寄存器)。线程在运行的过程中会把主内存的数据拷贝一份到线程内部cache中,也就是
working memory
。这个时候多个线程访问同一个变量,其实就是访问自己的内部cache。
- CPU的运行速度是远远高于内存的读写速度的,为了不让cpu为了等待读写内存数据,现代cpu和内存之间都存在一个高速缓存cache(实际上是一个多级寄存器)。线程在运行的过程中会把主内存的数据拷贝一份到线程内部cache中,也就是
- 除了cache的原因,重排序后的指令在多线程执行时也有可能导致内存不可见,由于指令顺序的调整,线程A读取某个变量的时候线程B可能还没有进行写入操作呢,虽然代码顺序上写操作是在前面的。
volatile原理
当一个变量被声明为volatile
时,在编译成会变指令的时候,会多出下面一行:
0x00bbacde: lock add1 $0x0,(%esp);
这句指令的意思就是在寄存器执行一个加0的空操作。不过这条指令的前面有一个lock(锁)前缀。
当处理器在处理拥有lock前缀的指令时:
在之前的处理中,lock会导致传输数据的总线被锁定,其他处理器都不能访问总线,从而保证处理lock指令的处理器能够独享操作数据所在的内存区域,而不会被其他处理所干扰。
但由于总线被锁住,其他处理器都会被堵住,从而影响了多处理器的执行效率。为了解决这个问题,在后来的处理器中,处理器遇到lock指令时不会再锁住总线,而是会检查数据所在的内存区域,如果该数据是在处理器的内部缓存中,则会锁定此缓存区域,处理完后把缓存写回到主存中,并且会利用缓存一致性协议
来保证其他处理器中的缓存数据的一致性。
缓存一致性协议
线程中的处理器会一直在总线上嗅探其内部缓存中的内存地址在其他处理器的操作情况,一旦嗅探到某处处理器打算修改其内存地址中的值,而该内存地址刚好也在自己的内部缓存中,那么处理器就会强制让自己对该缓存地址的无效。所以当该处理器要访问该数据的时候,由于发现自己缓存的数据无效了,就会去主存中访问。
使用volatile需要注意的
volatile
不能完全保证一个变量的线程安全。因为Java里面的运算并非是原子操作,所以导致volatile
声明的变量无法保证线程安全。
所谓原子操作:
一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。也就是说,处理器要么把这组操作全部执行完,中间不允许被其他操作所打断,要么这组操作不要执行。
其实大部分情况下volatile
还是可以保证线程安全的问题的,前提是满足以下条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
第一个条件的限制使volatile
变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入
操作序列组成的组合操作,必须以原子方式执行,而volatile
不能提供必须的原子特性。实现正确的操作需要使x的值在操作期间保持不变,而volatile
变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)
再来解释下第二个条件,如下代码,它包含了一个不变式:下界总是小于或等于上界。
// 非线程安全的数值范围类
@NotThreadSafe
public class NumberRange {
private int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
上面的代码中,如果将lower
、upper
定义为volatile
类型,并不能充分达到预期的线程安全的效果。假如凑巧两个线程在同一时间使用不一致的值执行setLower
和setUpper
的话,则会使范围处于不一致的状态。如果初始状态是 (0, 5)
(lower = 0, upper = 5),同一时间内,线程 A 调用 setLower(4)
并且线程 B 调用setUpper(3)
,显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是 (4, 3)
—— 一个无效值。至于针对范围的其他操作,我们需要使 setLower()
和 setUpper()
操作原子化 —— 而将字段定义为 volatile
类型是无法实现这一目的的。
关于如何正确使用volatile关键字,可以看下这片文章:volatile变量使用指南,关于volatile的使用讲解的非常清晰。