Java虚拟机规范定义的许多规则中的一条:所有对基本类型的操作除了某些对long类型和double类型的操作之外,都是原子级的;
当线程把主存中的 long/double类型的值读到线程内存中时,可能是两次32位值的写操作,显而易见,如果几个线程同时操作,那么就可能会出现高低2个32位值出错的情况发生。即long,double高低位问题,非线程安全
举例说明:
即如有一个long类型的field字段,某个线程正在执行 field = 123L ,而同时有另一个线程正在执行 field = 456L,这样的指定操作之后field的值会是什么,是无法保证的。也许是123L,也可能是456L,或许是0L,甚至还可能是31415926L
JVM内存模型中定义了8中原子操作
- lock:将一个变量标识为被一个线程独占状态
- unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定
- read:将一个变量的值从主内存传输到工作内存中,以便随后的load操作
- load:把read操作从主内存中得到的变量值放入工作内存的变量的副本中
- use:把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
- assign:把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,都要使用该操作
- store:把工作内存中的一个变量的值传递给主内存,以便随后的write操作
- write:把store操作从工作内存中得到的变量的值写到主内存中的变量
对于32位操作系统来说单次次操作能处理的最长长度为32bit,而long类型8字节64bit,所以对long的读写都要两条指令才能完成(即每次读写64bit中的32bit);如果JVM要保证long和double读写的原子性,势必要做额外的处理
public class LongAtomTest implements Runnable {
private static long field = 0;
private volatile long value;
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
public LongAtomTest(long value) {
this.setValue(value);
}
@Override
public void run() {
int i = 0;
while (i < 100000) {
LongAtomTest.field = this.getValue();
i++;
long temp = LongAtomTest.field;
if (temp != 1L && temp != -1L) {
System.out.println("出现错误结果" + temp);
System.exit(0);
}
}
System.out.println("运行正确");
}
public static void main(String[] args) throws InterruptedException {
// 获取并打印当前JVM是32位还是64位的
String arch = System.getProperty("sun.arch.data.model");
System.out.println(arch+"-bit");
LongAtomTest t1 = new LongAtomTest(1);
LongAtomTest t2 = new LongAtomTest(-1);
Thread T1 = new Thread(t1);
Thread T2 = new Thread(t2);
T1.start();
T2.start();
T1.join();
T2.join();
}
}
以上代码在32位JVM上和64位JVM上运行结果将不一致
从程序得到的结果来看,32位的HotSpot没有把long和double的读写实现为原子操作。 在读写的时候,分成两次操作,每次读写32位。因为采用了这种策略,所以64位的long和double的读与写都不是原子操作
结论
对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作;
如果使用volatile修饰long和double,那么其读写都是原子操作;
在实现JVM时,可以自由选择是否把读写long和double作为原子操作;
java中对于long和double类型的写操作不是原子操作,而是分成了两个32位的写操作。读操作是否也分成了两个32位的读呢?在JSR-133之前的规范中,读也是分成了两个32位的读,但是从JSR-133规范开始,即JDK5开始,读操作也都具有原子性;
java中对于其他类型的读写操作都是原子操作(除long和double类型以外);
对于引用类型的读写操作都是原子操作,无论引用类型的实际类型是32位的值还是64位的值;
对于long类型的不恰当操作可能读取到从未出现过的值。而对于int的不恰当操作则可能读取到旧的值;