Java的long、double类型的原子性读取问题
In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn’t happen at all. No side effects of an atomic action are visible until the action is complete.
以上是关于原子性的操作的相关描述。
在Java中,以下的操作可以认为是原子操作
- 对于引用变量、大多数的原始类型变量的读、写(所有的类型除了
long
和double
)都是原子性的(这个只有才32位的JVM成立) - 所有申明为
volatile
的变量(包括long
和double
变量)的读、写都是原子性的
在Java中,自增、自减操作都不是原子性操作,很容易理解。但是long
和duoble
的读写却不是原子性的问题,却不太好理解。
检验
在32位JVM上,通过两个线程对同一个成员变量进行读写,来测试long
和double
类型变量的读写不是原子性操作。
public class UnatomicLongDemo implements Runnable {
private static long test = 0;
private final long val;
public UnatomicLongDemo(long val) {
this.val = val;
}
@Override
public void run() {
while (!Thread.interrupted()) {
test = val;//两个线程同时断写test变量,如果test变量的读写操作是原子性的,那么test只能是-1或者0
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new UnatomicLongDemo(-1));
Thread t2 = new Thread(new UnatomicLongDemo(0));
System.out.println(Long.toBinaryString(-1));
System.out.println(pad(Long.toBinaryString(0), 64));
t1.start();
t2.start();
long switchVal;
while ((switchVal = test) == -1 || switchVal == 0) {
//如果test、switchVal的操作是原子性的,那么就应该是死循环,否则就会跳出该循环
System.out.println("testing...");
}
System.out.println(pad(Long.toBinaryString(switchVal), 64));
System.out.println(switchVal);
t1.interrupt();
t2.interrupt();
}
//将0补齐到固定长度
private static String pad(String s, int targetLength) {
int n = targetLength - s.length();
for (int x = 0; x < n; x++) {
s = "0" + s;
}
return s;
}
}
测试结果为
通过这个例子可以看出
-
在32位JVM上,对
long
型变量test
的读取或者对long
型变量switchVal
的写入不是原子性的。 -
非原子性的读、写只是造成
long
、double
类型变量的高、低32位数据不一致
这是由于在32位JVM中对64位的数据的读、写分两步,每一步读或者写32位的数据,这样就会造成两个线程对同一个变量的读写出现一个线程写高32位、另一个线程写入低32位数据。这样此变量的数据就出现不一致的情况。这时候volatile
关键字可以防止这种现象发生,因为java的内存模型保证了valotile
修饰的long
、double
变量的读写是原子性的。