注:每次使用volatile的时候,度娘和谷歌上的文章都是众说纷纭,没个重点,我还是自己动手丰衣足食吧。。。
volatile具备两种特性:
保证此变量对所有线程可见
public class Test {
private static volatile boolean flag = false;
private static final int TestCount = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[TestCount];
for (Thread testOne : threads) {
testOne = new Thread(
new Runnable() {
@Override
public void run() {
doWork();
}
});
testOne.start();
}
Thread testTwo = new Thread(new Runnable() {
@Override
public void run() {
setTrue();
}
});
testTwo.start();
}
public static void setTrue() {
System.out.print((flag = true)+" ");
}
public static void doWork() {
while (!flag) {}
System.out.print("stop ");
}
}
运行结果是一定的:
true stop stop stop stop stop stop stop stop stop stop
如果去掉flag变量的volatile标识, 则程序会陷入死循环状态 因为flag的变量不能立即被各个线程所得知。
由于volatile的可见特性,导致这样一种认知:“volatile变量对所有线程是立即可见的,对volatile变量的所有写的操作都能立即反应到其他线程之中”。在各线程内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此认为其是在内存中一直的。虽然如此,但是java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。比如:
public class Test {
private static volatile int count = 0;
private static final int TestCount = 20;
public static void main(String[] args) {
Thread testOne = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < TestCount; i++) {
selfAdd();
}
}
});
Thread testTwo = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < TestCount; i++) {
selfAdd();
}
}
});
testTwo.start();
testOne.start();
while (Thread.activeCount() > 1)
Thread.yield();
System.out.println(count);
}
public static void selfAdd() {
count = count + 1;
}
}
结果不一定是40,且都是小于40。
同样都是对数据进行同步操作,问什么会出现这个问题呢?
第一个例子: 对volatile的修改,能回写到主存中,其他线程都是直接从主存中读取。所以第一个程序正常结束。如果没有volatile标识,则flag的值由于内存优化,导致每个线程都有自己的变量副本,因此不会被其他线程及时读取,导致程序死锁。
第二个例子: 第一个线程对count进行了修改,但与此同时,count的值并没有回写到主存中去,但这时第二个线程已经读取了count的值准备运算,因此,出现数据不一致的情况。下面是selfAdd函数的字节码
public static void selfAdd();
Code:
0: getstatic #13; //Field count:I
3: iconst_1
4: iadd
5: putstatic #13; //Field count:I
8: return
可见count++是分成多步的,并不是一个原子操作。更何况字节码文件中的操作在转换机器码操作之后会有分成更多的步骤,所以count++这个操作不会立刻回写到主存中。
综上所述,由于volatile变量只能保证可见性,必须符合以下两条规则。
1.运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改变量的值
2.变量不需要与其他状态变量共同参与不变约束
禁止指令重排序优化
下文是关于上一个例子中 对count变量赋值的JIT源码
0x01a3de26: lock addl $0x0,(%esp)
这个指令的意思是添加内存屏障。有禁止指令重排的作用。比如:
指令2:a=a+10
指令3:b=b+2
显然指令1和2的顺序不可改变,但是指令3可以在任何时候执行,换句话说就是指令3可以在1之前,也可以在2之后,也可以在1和2中间。如果对b加以volatile标识即对b加以内存屏障,则b在回写时,会将本cpu的cache写入内存,此时本cpu的操作应该完成,也就是说指令1和2必须完成,才能将本cpu的内存回写。至此,volatile禁止指令重排的作用也就完成了。
名词解释
java内存模型
基于高速缓存的cache模型,能够有效的利用cpu资源,但是也带来了一个致命的问题:缓存一致性。在多处理机系统中,每个处理机都有自己的高速缓存,而他们又共享同一主内存。当多个处理机的运算任务都涉及到同一块主存区且回写数据时,该以那个处理机的中cache数据为准呢?java内存模型的主要目标是定义程序各个变量的访问规则。它规定了所有的变量都存储在主内存中(虚拟机内存的一部分,类比于上文提到的cache主存)。每条线程还有自己的工作内存(及上文提到的各cpu控制的各自的cache),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行。不同线程之间的工作内存是互不可见的,线程之间的交互均需要通过主内存来完成。很明显,java此处借鉴了操作系统的内存模式,除此之外,java也借鉴了操作系统中对内存的操作特性:原子性。java定义了八种原子操作,来完成主存与工作内存之间的读写。
内存屏障
只有一个cpu访问内存时,并不需要内存屏障,但如果有两个或更多cpu访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。
指令重排序
从硬件架构上讲,指令重排序是指cpu采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理。例如,在处理运算时,一个cpu有多个寄存器,cpu可以将不同的任务分发给不同的寄存器,导致了乱序。但这也加快了运算速度。
注:本文内容参考了周志明先生的《深入理解java虚拟机》