volatile关键字是最轻量的同步机制了。
volatile关键字修饰的变量有两个特性:
- 可见性:当一个线程修改了变量的的值,其他线程是可知的
- 禁止指令重排
Java里面的运算操作并不是原子操作,导致volatile变量的运算在并发下照样是不安全的。
只有符合下面两条规则的才是安全的:
- 运算的结果并不依赖于变量的当前值,或者保证只有单一线程修改变量的值
- 变量不需要与其他状态变量共同参与不变约束
下面是一个volatile的测试代码
package javathreadlearn;
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
/**
* public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
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 9: 0
line 10: 8
*/
/*运算不是原子操作,所以如果最终的结果依赖其余当前变量的值,就不可能得到正确的结果
当getstatic指令把race的值取到栈顶时,volatile关键字保证了race此时的值时正确的,但是在执行iconst_1,iadd这些指令时,其他线程
可能已经把race的值增大了,而在操作树栈顶的值就变成了过期的数据,所以putstatic指令执行后就有可能把较小的race值同步回主内存上
*/
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int i =0;i<10000;i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount()>1) {
Thread.yield();
}
System.out.println(race);
}
}
这段代码发起20个线程,每个线程对race变量进行10000次自增操作。如果代码能正确并发的话,最后输出应是200000。但运行后并不会收获预期结果。用javap反编译:
public static void increase();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
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 9: 0
line 10: 8
/*运算不是原子操作,所以如果最终的结果依赖其余当前变量的值,就不可能得到正确的结果
当getstatic指令把race的值取到栈顶时,volatile关键字保证了race此时的值时正确的,但是在执行iconst_1,iadd这些指令时,其他线程
可能已经把race的值增大了,而在操作树栈顶的值就变成了过期的数据,所以putstatic指令执行后就有可能把较小的race值同步回主内存上
*/
参考《深入理解Java虚拟机 第二版》