【JUC】i++ 源码详解

字节码自增指令

自增操作对应的JVM字节码为 iinc。下面逐个分析 i++ 操作和 ++i操作的字节码指令。

i++

1
2
3
4
public static void main(String[] args) {
int i = 0;
int a = i++;
}

上述代码的字节码解析:

  • 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 加一前的值。

image-20211021142230633

注意:iinc 1 by 1这条指令虽然在JVM层面只是一条命令,但是其在最底层的CPU层面却是包含了三个步骤:

  1. 读取:从局部变量表中读取 i 的值
  2. 累加:将该值进行加一
  3. 保存:将加一后的值保存回局部变量表中

其中的一条指令可以保证是原子操作,但是3条指令合在一起却不是,这就导致了i++语句不是原子操作。

如果在读取操作进行后,当前线程失去了执行权,那么在该线程再一次获取到执行权后,就不会再做一次读取操作,而是直接使用线程切换前读取到的值进行加一,这就导致了高并发下 i++ 操作的结果异常

i--

1
2
3
4
public static void main(String[] args) {
int i = 0;
int a = ++i;
}

image-20211021163147335

对比 ++ii++,可以看到二者的区别是:

  • ++i 是先执行 iinc 1 by 1,将 i 的值增加,然后再 iload_1 读取了加一后的 i 值,将该值赋给 a
  • i++ 则是在 iinc 1 by 1 执行前就先把 i 原本的值备份了一份,然后 i 自增后,再将之前备份的值赋给 a

补充:只有局部变量才使用 iinc 指令,如果是调用的成员变量属性++,则不会使用 iinc 指令,其指令更加复杂。先从局部变量表里 load 该值,然后再加一,最后存回局部变量表里去。

多线程场景下 i++ 的问题

情景:两个线程同时对 static int i = 0 进行各自连续100次的 i++ 操作,理想情况下结果为 200,但最极端的情况下,结果为 2。该过程图解:

幻灯片1