深入理解java中i++和++i的区别

老谭带你深入理解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底层指令实现的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老谭酸菜面

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值