目录
volatile是java虚拟机提供的轻量级同步规则。它具备两种特性
保证被volatile修饰的变量对所有线程的可见性。
可见性是指当一条线程修改了这个变量的值的时候,新值对于其他线程来说是可以立即得知的。普通变量做不到这一点。
但需要注意的是,这并不能得出:volatile变量的运算在并发下是安全的这个结论。原因是java里面的运算并不是原子操作,导致volatile变量的运算在并发下一样是不安全的。
可以通过一个例子说明这个问题:
public class VolatileTest {
public static volatile int race = 0;
private static void increase(){
race++;//非原子操作
}
//final is always the first
private static final int THREAD_COUNT = 20;
public static void main(String[] args){
Thread[] threads = new Thread[THREAD_COUNT];
for(int i=0; i<threads.length; i++){
threads[i]= new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i<10000;i++){
//race++;
increase();
}
}
});
threads[i].start();
}
//等待所有线程累加结束
while (Thread.activeCount()>1){
Thread.yield();
System.out.println(race);
}
}
}
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作。如果这段代码能够正确并发的话,最后输出的结果应该是200000. 但是运行完之后并不会得到正确的结果,并且每次的输出的结果都不一样,都是一个小于200000的数字,这是为什么呢?
问题出在自增运算"race++"之中,利用javap反编译这段代码后会得到如下代码:
public static void increase();
Code:
Stack=2, Locals=0, Args_size=0
0: getstatic #13; //Field race:I
3: iconst_1
4: iadd
5: putstatic #13; //Field race:I
8: return
LineNumberTable:
Line 14: 0
Line 15: 8
发现代码increase()虽然只有一行,但是在Class文件是由4条字节码指令构成的。从字节码层面上很容易就分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但在执行iconst_1,iadd这些指令的时候,其他线程可能已经别把race的值加大了饿,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同回内存之中。
事实上这仍然是不严谨的,因为即时编译出来只有一条字节码指令,也并不一位执行这条指令就是一个原子操作。
由于volatile变量只能保证可见性,只有在以下两条规则的运算场景中,在其他场景仍然要通过加锁来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程改变变量的值。
- 变量不需要与其他状态变量共同参与不变约束。
除了volatile之外,java还有两个关键字能实现可见性,即synchronized和final。
第二个语义是禁止指令重排序优化
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果。而不能保证变量赋值操作顺序与程序代码中的执行顺序一致。在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的"线程内表现为串行的语义“。
在代码的例子当中就是一句在后面的代码可能会提前执行,而volatile关键字则可以避免此类情况的发生。在使用volatile修饰的变量,复制后,字节码里面会多一个”lock add1“操作,它相当于一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置,相当于设了一个重排序的界限。
问题
Java 中能创建 volatile 数组吗?
能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了。
volatile 能使得一个非原子操作变成原子操作吗?
对volatile字段的操作并不是原子的。比这更糟糕的是,经常结果是一样的,没有看出它不是原子性操作。如:可能volatile字段在多线程下正常运行了很多年,一次偶然的改变可能就会导致突然的崩溃。
一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么?因为 Java 中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子。
在本篇博客第二部分的内容当中的例子程序就证明了这一点。
volatile 修饰符的实际应用?
一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。
volatile 类型变量提供什么保证?
volatile 变量提供顺序和可见性保证,例如,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的。