为啥i++并不是原子性操作
在编写多线程程序时,对于不是原子操作的,需要引起程序员的额外注意。
一定要确保其对数据的操作是同步的,否则会引发数据安全问题。
i++不是原子操作
先来看一个例子,多线程下出现的数据不一致问题。
public class Test {
static int i = 0;
public static void main(String[] args) {
for (int j = 0; j < 100; j++) {
new Thread(() -> {
//休眠5毫秒,结果更明显
SleepUtil.MILL.sleep(5);
i++;
}).start();
}
SleepUtil.SEC.sleep(1);
System.out.println(i);
}
}
123456789101112131415
输出如下:
93 //输出的值不确定
1
启动100个线程去对i++,最终结果却是小于100。
为什么不是原子操作
再来看一个例子。
public class Test {
static int i = 0;
public static void main(String[] args) {
i++;
}
}
123456
将Test.java编译成class文件,然后利用javap -c 进行反汇编。
获得如下文件:
Compiled from "Test.java"
public class item02.tool.Test {
static int i;
public item02.tool.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
//获取指定类的静态域,并将其值压入栈顶
0: getstatic #2 // Field i:I
//将int型1推送至栈顶
3: iconst_1
//将栈顶两int型数值相加并将结果压入栈顶
4: iadd
//为指定的类的静态域赋值
5: putstatic #2 // Field i:I
8: return
static {};
Code:
0: iconst_0
1: putstatic #2 // Field i:I
4: return
}
12345678910111213141516171819202122232425262728
Java代码最终会被编译成一条条指令,线程在执行每一条指令前都随时有可能会失去CPU的执行权。
对于i++操作,可以看到最终被编译成如下四条指令:
//获取指定类的静态域,并将其值压入栈顶
0: getstatic #2 // Field i:I
//将int型1推送至栈顶
3: iconst_1
//将栈顶两int型数值相加并将结果压入栈顶
4: iadd
//为指定的类的静态域赋值
5: putstatic #2 // Field i:I
12345678
先取值,然后加1,最后赋值给静态变量。
线程很可能取完值,就失去了CPU的执行权,然后其他线程取得了相同的i值,导致即使多个线程+1,其实最终结果也只加了1而已,最终输出i就小于100了。
使用字节码来分析并发问题并不严谨,因为即使编译出来只有一条JVM指令,也并不意味着这条指令的操作是原子的。一条JVM指令在解释执行时,解释器需要执行很多行代码才能完成它的语义。如果是编译执行,一条JVM指令也可能被编译成多行本地机器码指令。如果需要更加严谨的分析,可以通过反汇编