为什么源码中喜欢额外声明一个局部变量来使用?

起因

最近在阅读一些源码,发现一个有趣的事情,很多源码中的方法,喜欢单独声明一个局部变量来进行后续的操作。比如:

在JDK的源码中,经常可以看到这种操作。(下图蓝色变量是实例变量,其他都是局部变量)

比如Arrylist的next方法:

ArrayList.this是内部类中对外部类对象的this引用

再比如JUC的LongAdder中的sum方法:

 

奇妙的是,这两个方法中,都声明了一个单独的局部变量,并将对象字段值都赋予其

疑惑

刚看到这些代码,会觉得疑惑,这岂不是多此一举?为什么不能直接使用实例变量进行操作呢?但是这毕竟是JDK的源码。这么做,肯定是有理由的。

其实稍微想想,也大概能猜到原因。如果每次操作,都使用this.实例字段的方式,肯定有一个不断获取对象值的操作,如果直接声明一个局部变量,是不是就可能避免了后续取值了呢?直接使用局部变量就好了!

破案

现在回到这两个方法,他们二者的区别就在于:第二个方法func2()中使用了一个局部变量。

看一下字节码指令:

 

好玩的是,多了一个局部变量,貌似字节码指令还少一些。我们重点看一下sum这个地方的操作:

1.如果不使用局部变量

 

 2.使用了局部变量后

我们可以清楚地发现,如果使用了局部变量,可以直接操作局部变量,也就避免了重复的getfield指令取数组。而getfield是需要去堆区找到对象,然后取出对象的实例字段。说白了,避免了一次访存操作,因为之前缓存到本地变量表里的局部变量c中了。

我们知道,访存是一个特别耗时的操作(采用句柄结构的话,甚至需要两次访存),这也是为什么现代计算机需要L1、L2、L3等告诉缓存区。而事先存储到本地变量后,就避免避免了一次访存,直从局部变量表中取出操作数。

以寄存器操作时50ns为例,访存可能需要200ns以上。

当然,这也是典型的空间换时间,性能的提升避免不了带来两个问题的思考:

问题一:我们把堆中的数据,暂存到栈里的局部变量表中,这是不是要多一份空间的消耗?

这个问题并不算大,甚至不算是问题。因为额外的空间是开辟在局部变量表(或称本地变量表,所缩写为LV)中的。我们知道,每个线程都有自己的虚拟机栈空间,而方法执行是依赖栈帧来运行的,本地变量表,是栈帧中的一个数据结构,用来暂存本次方法执行所需的局部变量。方法执行结束,栈帧出栈。并不会一直消耗我们的内存。

问题二:暂存到局部变量中,会不会有并发脏读问题?

答案是肯定的。没有使用任何同步手段的情况下,并发问题一定不可避免。但是抛开业务谈技术,没有任何意义。我们考虑一下这个业务场景:累加数组元素的值,这种情况下,数组元素的值是否会变化?如果实收尾性的累加和统计工作,前提一定是这个数组不会再变动了,业务上已经到达了收尾和整理阶段。但是如果就是并发态下的实例字段引用,那确实就需要加锁或者CAS的手段,保证数据的同步和即时性。

 

 


版权声明:本文为Myiad_wly原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。