java趣谈(2)说说自增那些事

最近,做题的时候经常遇到自增的问题,就此总结整理一下


一、常见情况

自增方式有两种:前置和后置,即i++和++i,它们的不同点在于i++是在程序执行完毕后进行自增,而++i是在程序开始执行前进行自增,示例如下:

public static void main(String[] args){
		int i = 1;
		System.out.println(i++ + i++);
		System.out.println("i=" + i);
		System.out.println(i++ + ++i);
		System.out.println("i=" + i);
		System.out.println(i++ + i++ + i++);
		System.out.println("i=" + i);
	}

结果如下


不明白的话可看下面的分析,看懂的直接跳过即可

表达式 i++ + i++首先执行第一个i++操作,由于自增操作会稍后执行,因此,运算时,i的值还是1,但自增操作后,i的值变为了2,接着执行第二个i++,运算时,i的值已经为2了,二执行了一个自增操作后,i的值变为了3,所以i++ + i++ = 1 + 2 =3,而运算完成后,i的值变为3

表达式 i++ + ++i首先执行第一个i++,但是自增操作后稍后执行。因此,此时i的值还为3,接着执行++i,此时i的值变为4,同时还要补执行i++的自增操作,所以i++ + ++i = 3 + 5 = 8

同理,i++ + i++ + i++ = 5 + 6 + 7 =18



二、循环中的自增

for循环中,由于for循环的第三个条件是最后判断(先执行循环语句,再执行第三个条件),所以i++和++i无甚区别,上代码说明:




while循环中,要首先判断括号内的条件,所以++i相比i++,少执行一次





三、a=a++?

最初看到这个表达式,没在意,以为和a++一个意思,后来跑了一段代码才发现不对劲

上代码





简单版的解释:可以理解为中间缓存变量机制

a = a++ 等同于

temp = a;

i = i + 1;

i = temp;

而a = ++a 等同于

a = a + 1;

temp = i;

i = temp;


深层次解释(结合jvm解释):

第1步:将int类型的0入栈,就是放到操作数栈的栈顶
第2步:将操作数栈栈顶的值0弹出,保存到局部变量表 index (索引)值为1的位置。(局部变量表也是从0开始的,0位置一般保存当前实例的this引用,当然静态方法例外,因为静态方法是类方法而不是实例方法)
第3步:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)
第4步:int类型的值进行自增操作,(这时局部变量表index为1的值因为执行了自增操作变为1了,但是操作数栈中栈顶的值仍然是0)
第5步:将操作数栈顶的值弹出(值0),放到局部变量表index为1的位置(旧值:1,新值:0),覆盖了上一步局部变量表的计算结果。
第6步:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)

总结:从执行顺序可以看到,这里第2和第5步执行了2次将0赋值给变量i的操作,i++操作是在这两次操作之间执行的,自增操作是对局部变量表中的值进行自增,而栈顶的值没有发生变化,这里需要注意的是保存这个初始值的地方是操作数栈而不是局部变量表,最后再将栈顶的值覆盖到局部变量表i所在的索引位置中去。


四、i++是线程安全的吗?

看到这个问题一脸蒙逼,i++ 居然还有线程安全问题?

只能说自己了解的不够多,水平有限

假设场景,1000个线程,每个线程对共享变量 count 进行 1000 次 ++ 操作

上代码

	static int count = 0;
	static CountDownLatch cd1 = new CountDownLatch(1000);
	public static void main(String[] args) throws Exception{
		CountRunnable countRunnable = new CountRunnable();
		for (int i = 0; i <1000; i++) {
			new Thread(countRunnable).start();
		}
		cd1.await();
		System.out.println(count);
	}
	static class CountRunnable implements Runnable{
		private void count(){
			for (int i = 0; i < 1000; i++) {
				count++;
			}
		}
		@Override
		public void run(){
			count();
			cd1.countDown();
		}
	}

第一次输出结果:995904

第一次输出结果:997097

第一次输出结果:999681

第一次输出结果:995186

第一次输出结果:998063

......

期望的结果应该是 1000000,但运行 N 遍,发现总是不为 1000000

所以,i++ 操作它不是线程安全的


每个线程都有自己的工作内存,每个线程需要对共享变量操作时必须先把共享变量从主内存 load 到自己的工作内存,等完成对共享变量的操作时再 save 到主内存。

问题就出在这了,如果一个线程运算完后还没刷到主内存,此时这个共享变量的值被另外一个线程从主内存读取到了,这个时候读取的数据就是脏数据了,它会覆盖其他线程计算完的值。。。

这也是经典的内存不可见问题,那么把 count 加上 volatile 让内存可见是否能解决这个问题呢? 答案是:不能。因为 volatile 只能保证可见性,不能保证原子性。多个线程同时读取这个共享变量的值,就算保证其他线程修改的可见性,也不能保证线程之间读取到同样的值然后相互覆盖对方的值的情况。


解决方案

对 i++ 操作的方法加同步锁,同时只能有一个线程执行 i++ 操作;

如图所示





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值