记得刚工作的时候被问i++是不是原子操作。初出茅庐答不上来,对java并发不了解。以此笔记缅怀自己的年轻。
看下面的代码
public class Test {
public static void main(String[] args) {
int i = 8;
i = i++;
System.out.println(i);
}
}
打印的结果是啥? 答案是8. 换成下面的代码
public class Test {
public static void main(String[] args) {
int i = 8;
i = ++i;
System.out.println(i);
}
}
答案是9。
是不是很无聊,相信很多人会回答,i++是先赋值再+1,++i是先+1再赋值。确实是这样,但是我总是想追根溯源,如何解释这个原则?
打开IDEA,下载一个jclasslib插件。
利用这个工具查看字节码文件,就是class文件。
右边就是显示这个类的class字节码信息,字节码其实是一堆二进制数据,当然,这些二进制数据按一定的规则存放,这个插件就是简单根据规则翻译了这个class文件,我们先看i=i++。 我们找到main方法对应的字节码
image.png
看到了吗,main方法的执行过程对应的jvm字节码指令就是
0 bipush 8
2 istore_1
3 iload_1
4 iinc 1 by 1
7 istore_1
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
首先解释一下什么是字节码指令,你写的java程序,JVM翻译成指令程序,指令程序最后再翻译成计算机认识的二进制程序。 毕竟计算机不认识你的java程序。因为它不是人。
要理解这个字节码,首先要明白,方法的执行就是操作栈帧,栈帧中包含了操作数栈和本地变量表这两个概念。 本地变量表,就是保存了方法的变量,比如第0位置的args参数,第1位置的i参数。操作数栈你可以理解对数据进行压栈出栈操作。 bipush 8就是把8压到操作数栈中。 istore_1就是操作数栈出栈,存到本地变量表的第1位置的i,就是代码中的i = 8; iload_1,就是变量表中第一个位置的i压栈到操作数栈顶,此时栈顶为8 iinc 1 by 1,就是变量表中第一个位置的i加1,那么变量表最终i=9 istore_1,又把栈顶的8存回了变量表中的i,那么i=8; 因为java代码中又赋值给了i。 所以最终打印8。
再看i=++i的字节码指令
0 bipush 8 把8压到操作数栈中
2 istore_1 操作数栈出栈,存到本地变量表的第1位置的i,就是代码中的i = 8;
3 iinc 1 by 1 变量表中第一个位置的i加1,那么变量表最终i=9
6 iload_1 变量表中第一个位置的i压栈到操作数栈顶,此时栈顶为9
7 istore_1 又把栈顶的9存回了变量表中的i,那么i=9;
8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #3 <java/io/PrintStream.println>
15 return
关于i++,和++i的字节码指令JVM就是这么规定的。从表面上看就是上文说的,i=i++先赋值再加1,i=++i先加1再赋值。
曾经的那个面试官,你在哪儿,我来回答你当年那个问题,可能你并不知道也停留在表面吧。