【JUC】i++ 源码详解
字节码自增指令
自增操作对应的JVM字节码为 iinc
。下面逐个分析 i++
操作和 ++i
操作的字节码指令。
i++
1 | public static void main(String[] args) { |
上述代码的字节码解析:
iconst_0
:将数字 0 入操作数栈istore_1
:弹出操作数栈顶元素 0 并存储到局部变量表索引位置为1的变量(此时操作数栈为空)iload_1
:从局部变量表中加载索引位置1的元素(0)到操作数栈(目的是备份一下i 自增前的值,待自增结束后再赋值给 a)iinc 1 by 1
:首先读取局部变量表中 i 的值,再进行加一操作,最后存储加一后的值1到索引位置 1,重新赋给了 i(该指令没有原子性)istore_2
:将操作数栈顶的元素 0(第三步存储的)赋值给局部变量表中的 a
所以最终结果就是,a 的值等于 0。而到这这一结果的根本原因是因为:在给 a 赋值前先从局部变量表中 load
了 i 加一前的值,并在最后将该值又赋给了 a,所以造成了 i++
后 a 的值等于 i 加一前的值。
注意:iinc 1 by 1
这条指令虽然在JVM层面只是一条命令,但是其在最底层的CPU层面却是包含了三个步骤:
- 读取:从局部变量表中读取 i 的值
- 累加:将该值进行加一
- 保存:将加一后的值保存回局部变量表中
其中的一条指令可以保证是原子操作,但是3条指令合在一起却不是,这就导致了i++
语句不是原子操作。
如果在读取操作进行后,当前线程失去了执行权,那么在该线程再一次获取到执行权后,就不会再做一次读取操作,而是直接使用线程切换前读取到的值进行加一,这就导致了高并发下 i++
操作的结果异常
i--
1 | public static void main(String[] args) { |
对比 ++i
和 i++
,可以看到二者的区别是:
++i
是先执行iinc 1 by 1
,将 i 的值增加,然后再iload_1
读取了加一后的 i 值,将该值赋给 ai++
则是在iinc 1 by 1
执行前就先把 i 原本的值备份了一份,然后 i 自增后,再将之前备份的值赋给 a
补充:只有局部变量才使用 iinc 指令,如果是调用的成员变量属性++,则不会使用 iinc 指令,其指令更加复杂。先从局部变量表里 load 该值,然后再加一,最后存回局部变量表里去。
多线程场景下 i++ 的问题
情景:两个线程同时对 static int i = 0
进行各自连续100次的 i++
操作,理想情况下结果为 200,但最极端的情况下,结果为 2。该过程图解: