老谭带你深入理解java中i++和++i的区别
最近老谭在面试中频频遇到i++和++i之类的基础题目,但是细思之下,其实这道题目可是一点也不简单呐,其中涉及:java基础运算操作、JVM运行时环境、JVM栈内存操作、JVM指令、甚至并发多线程等。下面我就来谈谈我对此的理解(如有理解不到位的地方请指教,如有收货也请不要吝啬您的赞,因为您小手一挥就是对我莫大的支持)。
PS:①:java源文件经javac编译后生成*.class文件
②:javap -c 生成的*.class文件,则可以查看jvm虚拟机指令
1)面试题分析
首先话不多说,撸起袖子就是干,先甩出几道题留给感兴趣的您:
@Test
public void test01(){
int i = 0;
for (int j = 0; j < 10; j++) {
i = i++;
}
System.out.println("i = "+ i);//???
}
@Test
public void test02(){
int i = 0;
for (int j = 0; j < 10; j++) {
i = ++i;
}
System.out.println("i = "+ i);//???
}
@Test
public void test03(){
Integer i = 0;
for (int j = 0; j < 10; j++) {
i = i++;
}
System.out.println("i = "+ i);
}
@Test
public void test04(){
Integer a = 0;
int b = 0;
for (int i = 0; i < 10; i++) {
a = a++;
b = a++;
}
System.out.println("a = "+a);
System.out.println("b = "+b);
}
输出结果分别是:
test01()
i = 0
test02()
i = 10
test03()
i = 0
test04():
a = 10
b = 9
是不是和你的预期结果大相径庭呢?下面让老谭带你深入理解why?以下面这道题来分析:
@Test
public void test05(){
int i = 1;//①
i = i++;//②
int j = i++;//③
int k = i + ++i * i++;//④
System.out.println("i = "+ i);//i = ****
System.out.println("j = "+ j);//j = *
System.out.println("k = "+ k);//k = ***** ***** *
}
输出结果:
i = 4
j = 1
k = 11
/**
* 详细过程分析(可参考JVM虚拟机规范关于JVM指令部分):
* 〇此方法被调用时test01方法被压入java栈内存中的栈帧(栈帧中可存在局部变量表、
* 操作数栈、动态链接、方法出口等);
* 〇i++和++i二者的区别是:i++先赋值后自增,++i是先自增后赋值.
* ①在局部变量表中定义变量i=1;
* ---------------------------------------------------------
* ②i = i++;拆解为jvm虚拟机指令来讲,即:
* ---------------------------------------------------------
* 1.把局部变量i的值压入操作数栈
* 局部变量表 操作数栈
* int i=1 1
* 2.i局部变量自增1
* 局部变量表 操作数栈
* int i=2 1
* 3.把操作数栈中的值赋值给局部变量i
* 局部变量表 操作数栈
* int i=1
* 从上面的分析可以看出,在运算过程中局部变量i确实是有变为2过,但是在赋值时又被操作数栈中
* 之前压入栈中的值给覆盖了,所以最终局部变量i的结果还是1.
* ---------------------------------------------------------
* ③int j = i++;拆解为jvm虚拟机指令来讲,即:
* 1.把局部变量i的值压入操作数栈
* 局部变量表 操作数栈
* int i=1 1
* int j
* 2.i局部变量自增1
* 局部变量表 操作数栈
* int i=2 1
* int j
* 3.把操作数栈中的值赋值给局部变量j
* 局部变量表 操作数栈
* int i=2
* int j=1
* 从上面的分析可以知道,这次操作数栈的值没有赋值给i而是赋值给了新的变量j,所以i=2;j=1;
* ---------------------------------------------------------
* ④int k = i + ++i * i++;拆解为jvm虚拟机指令来讲,即:
* (个人理解虽然是先算乘除再加减,但是得先有数才能运算,所以先从左至右把操作数压入栈)
* 1.把局部变量i的值压入操作数栈(i)
* 局部变量表 操作数栈
* int i=2 2
* int j=1
* int k
* 2.局部变量i先自增1,然后再把局部变量i压入栈(++i)
* 局部变量表 操作数栈
* int i=3 3
* int j=1 2
* int k
* 3.把局部变量i的值先压入操作数栈,然后局部变量自增1(i++)
* 局部变量表 操作数栈
* int i=4 3
* int j=1 3
* int k 2
* 4.操作数栈中的前两个数(从栈顶开始)取出求乘积运算,然后再把临时结果压入栈
* 局部变量表 操作数栈
* int i=4 9
* int j=1 2
* int k
* 5.再把操作数栈中的两个数取出求和再赋值给k
* 局部变量表 操作数栈
* int i=4
* int j=1
* int k=11
* 总结:
* 1)"="赋值运算最后计算
* 2)"="赋值运算右边的规则是:从左至右加载变量值依次压入操作数栈
* 3)实际先算哪个,看运算符的优先级
* 4)自增、自减操作都是直接修改变量的值,不经过操作数栈
* 5)最后的赋值之前,临时结果也是存储在操作数栈中
*/
好了,看了上面这道题的讲解之后,你是不是已经有了初步的认识了呢?要不来练练手?
@Test
public void test06(){
System.out.println("IncTest() return : "+ IncTest());//IncTest() return :???
}
public int IncTest(){
int i = 1;
i++;//**
++i;//***
try {
if(i/0 >0){
i++;
}
} catch (Exception e) {
i++;//****
}finally {
i++;//*****
}
return ++i;//返回******
// return i++;//返回*****
}
输出结果:
return ++i;时输出:
IncTest() return : 6 //i先加1,再返回i的值
return i++;时输出:
IncTest() return : 5 //先返回i的值,i再加1
哈哈,是不是又答错了,这里还有个小坑坑,要自己体会哟!比如:
@Test
public void test09(){
int i = 0;
int j = 0;
System.out.println("i = " + i++);
System.out.println("j = " + ++j);
}
2)i++和++i的实现原理:
/**i++和++i的实现原理:
* ①:javac编译后生成*.class文件
* ②:javap -c 生成的*.class文件,则可以查看jvm虚拟机指令如下:
* =====================================
* int i = 0;
* int j = i++;
*
* 0: iconst_0 // 生成整数0
* 1: istore_1 // 将整数0赋值给1号存储单元(即变量i)
* 2: iload_1 // 将1号存储单元的值加载到数据栈(此时 i=0,栈顶值为0)
* 3: iinc 1, 1 // 1号存储单元的值+1(此时 i=1)
* 6: istore_2 // 将数据栈顶的值(0)取出来赋值给2号存储单元(即变量j,此时i=1,j=0)
* 7: return // 返回时:i=1,j=0
* =====================================
* int i = 0;
* int j = ++i;
*
* 0: iconst_0 // 生成整数0
* 1: istore_1 // 将整数0赋值给1号存储单元(即变量i)
* 2: iinc 1, 1 // 1号存储单元的值+1(此时 i=1)
* 5: iload_1 // 将1号存储单元的值加载到数据栈(此时 i=1,栈顶值为1)
* 6: istore_2 // 将数据栈顶的值(1)取出来赋值给2号存储单元(即变量j,此时i=1,j=1)
* 7: return // 返回时:i=1,j=1
*/
3)多线程并发引发的混乱
在多线程环境下由++i或者++i操作都会引起的数据混乱。引发混乱的原因是:++i和i++操作不是原子操作。虽然在Java中++i是一条语句,字节码层面上也是对应iinc这条JVM指令,但是从最底层的CPU层面上来说,++i操作大致可以分解为以下3个指令:
1.取数
2.累加
3.存储
其中的每一条指令可以保证是原子操作,但是3条指令合在一起却不是,这就导致了++i语句不是原子操作。如果变量i用volatile修饰是否可以保证++i是原子操作呢,实际上这也是不行的。至于原因,详见下面题外话。
如果要保证累加操作的原子性,可以采取下面的方法:
1.将++i置于同步块中,可以是synchronized或者J.U.C中的排他锁(如ReentrantLock等)。
2.使用原子性(Atomic)类替换++i,具体使用哪个类由变量类型决定。如果i是整形,则使用AtomicInteger类,其中的AtomicInteger#addAndGet()就对应着++i语句,它是原子性操作。
@Test
public void test07(){
AtomicInteger atomicInteger = new AtomicInteger(0);
System.out.println(atomicInteger.get());//0
atomicInteger.set(1);
System.out.println(atomicInteger.get());//1
System.out.println(atomicInteger.addAndGet(1));//2
System.out.println(atomicInteger.incrementAndGet());//3
System.out.println(atomicInteger.decrementAndGet());//2
System.out.println(atomicInteger.getAndDecrement());//2
System.out.println(atomicInteger.getAndIncrement());//1
System.out.println(atomicInteger.get());//2
}
4)题外话
volatile关键字
volatile是一个特殊的修饰符,只有成员变量才能使用它,与Synchronized及ReentrantLock等提供的互斥相比,Synchronized保证了Synchronized同步块中变量的可见性,而volatile则是保证了所修饰变量的可见性。可见性指的是在一个线程中修改变量的值以后,在其他线程中能够看到这个值(在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的(不可见))。因为volatile只是保证了同一个变量在多线程中的可见性,所以它更多是用于修饰作为开关状态的变量。
java关键字volatile,从表面意思上是说这个变量是易变的,不稳定的,事实上,确实如此,这个关键字的作用就是告诉编译器,凡是被该关键字声明的变量都是易变的、不稳定的。所以不要试图对该变量使用缓存等优化机制,而应当每次都从它的内存地址中去读值。使用volatile标记的变量在读取或写入时不需要使用锁,这将减少产生死锁的概率,使代码保持简洁。
请注意,这里只是说每次读取volatile的变量时都要从它的内存地址中读取,并没有说每次修改完volatile的变量后都要立刻将它的值写回内存。也就是说volatile只提供了内存可见性,而没有提供原子性,操作互斥提供了操作整体的原子性,同一个变量多个线程间的可见性与多个线程中操作互斥是两件事情,所以说如果用这个关键字做高并发的安全机制的话是不可靠的。
volatile的用法如下:
public volatile static int count=0;//在声明的时候带上volatile关键字即可
什么时候使用volatile关键字?当我们知道了volatile的作用,我们也就知道了它应该用在哪些地方,很显然,最好是那种只有一个线程修改变量,多个线程读取变量的地方。也就是对内存可见性要求高,而对原子性要求低的地方。
从上面的描述中,我们可以看出volatile与加锁机制的主要区别是:加锁机制既可以确保可见性又可以确保原子性,而volatile变量只有确保可见性。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
详见:https://www.cnblogs.com/dolphin0520/p/3920373.html
(1)http://www.importnew.com/18126.html
(2)http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
(3)http://www.importnew.com/20594.html
原子操作:atomic
atomic是不会阻塞线程,线程安全的加强版的volatile原子操作。
原子操作的实现原理:
是利用CPU进行交换比较(即CAS:Compare and Swap)和非阻塞式算法(nonblocking algorithms);查看AtomicInteger源码会发现有些是通过调用JNI的代码实现的。JNI(Java Native Interface)为JAVA的本地调用,允许java调用其它语言,而Compare and Swap就是借助C来调用CPU底层指令实现的。