在解释这个问题之前,我们先来回顾下Java中基础数据类型所占的位数。
类型 | 长度(位) | 字节 |
---|---|---|
byte | 4 | 1 |
boolean | 4 | 1 |
int | 32 | 4 |
short | 16 | 2 |
long | 64 | 8 |
char | 16 | 2 |
float | 32 | 4 |
double | 64 | 8 |
可以看到对于double以及long两种基本数据类型,所占位数为64位。
而JVM却有32bit与64bit两种,也就是说在32bit JVM中不能将double 与 long类型的数据在一条指令中进行处理。为了处理64bit的数据,32bit的JVM会将一次对64bit的数据操作变为两次对32bit的操作。
具体来说,64bit的普通 long和double 数据在 32bit的JVM中 会被分为上32bit与下32bit。
在一次读或写中,JVM将会分别读取 下32bit与上32bit。
代码演示
/**
* double long 并发读写问题
*
* @author eventime
*/
public class DoubleLongTest {
static long number = 0L;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
int i = 0;
while (i < 100000) {
i ++;
number = 4294967296L;
long temp = number;
if (temp != 4294967296L && temp != 1L) {
System.out.println(temp);
System.out.println("出现并发错误");
System.out.println("test1:" + i);
System.exit(0);
}
}
});
Thread t2 = new Thread(() -> {
int i = 0;
while (i < 100000) {
i ++;
number = 1L;
long temp = number;
if (temp != 4294967296L && temp != 1L) {
System.out.println(temp);
System.out.println("出现并发错误");
System.out.println("test2:" + i);
System.exit(0);
}
}
});
String arch = System.getProperty("sun.arch.data.model");
System.out.println(arch+"-bit");
t2.start();
t1.start();
t1.join();
t2.join();
}
}
上文的代码中,我定义了t1,t2两个线程。两线程都是更改一个不加锁的静态变量number并且判断输出。
运行程序我们可能得到下面的结果:
32-bit
出现并发错误
错误值为:0
32-bit
出现并发错误
错误值为:4294967297
32-bit
如果还记并发编程中的竞态条件问题,按理来说上面程序在启动后 number应该为4294967296L或者1L。但是为什么会出现0L和:4294967297L呢?
让我们观察两个线程的具体执行过程:
t1负责每次循环将静态变量number赋值为 4294967296L, 这个数字的二进制表示为
000000000000000000000000000000001 0000000000000000000000000000000
可以看到他的下32位全部为0;
t2负责每次循环将静态变量number赋值为 1L, 这个数字的二进制表示为
000000000000000000000000000000000 0000000000000000000000000000001
可以看到他的上32位全部为0;
在线程的执行过程中,由于long这样的64bit数据读写并不是原子的,所以对其数据的操作将会分为上32bit与下32bit。
正常情况如图所示,操作时序图期望应该是这样,读出的数据要么是1L要么是4294967296L。
但是由于写入和读取并非原子操作,所以可能出现以下的时序错误:
得到4294967297L
得到0L
解决方案
解决double long非原子读写问题的方案很简单。
- 使用64bit的JVM,在64bit中,对于64bit的数据读写默认为原子类型。
- 在32bit的JVM中,对long double这样的数据添加 volatile 关键字。添加后JVM便会将double long的读写变为原子读写。