java基础教程栏目为大家介绍i++引发的bug。
大家好,作为日常写bug修bug的我,今天给大家带来前几天刚刚修复的一个事故。不得不承认,有我的地方总是会有这么多bug。
起因
故事的开始发生在前几天,有一个不是很常用的导出功能,被用户反馈出,不管条件是怎么样,导出的数据只有一条,但是实际上根据条件查询是有很多数据,而且页面中也查询出很多数据。(这个问题已经被修复了,所以当时的Kibana日志也找不到了)于是放下手上的工作,投入其中来看这个问题。
分析
从问题的描述中来分析,那么只可能出现在以下情况:
- 根据搜索条件查询出来的记录只有一条。
- 对查询出来的数据进行相关业务处理,导致最后的结果只有一条。
- 文件导出组件的逻辑处理之后,导致结果只有一条。
题外话
写到了这里,突然想到了一个经典面试题,MQ消息丢失的场景原因分析。哈哈哈,其实大致上也是这么几个角度分析。(有机会来写MQ的文章)
题外话
于是就一个个来分析:
- 找到相关业务的SQL以及对应的参数,查询可得,数据不止1条,所以第一种情况可以排除。
- 中间业务当中有涉及到相关权限、数据敏感等,将这些都放开之后,还是只有1条数据。
- 文件导出组件在接收到数据的时候,打印出的日志也显示只有一条,那么可以说明肯定中间相关业务的逻辑发生了问题。
由于这段代码都是写在一整个方法里面,导致Arthas排查起来就比较困难,只好一步步设置日志进行排查。(所以,如果是一大段逻辑,建议是拆分成duoge 子方法,一来在写的时候思路明确,有一种模块化的概念,至于方法复用什么的我就不多提了,基本操作;二是一但发生问题,排查起来也会方便点,经验之谈)。
最终定位到一个for循环里面。
代码
话不多说,我们直接来看代码。众所周知,我向来是一个很保护公司代码的人,所以,我在这里又不得不给大家模拟一下了。从问题的情况来看,是导出的对象记录是空
import com.google.common.collect.Lists;import java.util.List;public class Test { public static void main(String[] args) { // 获取Customer数据,这里就简单模拟 List<Customer> customerList = Lists.newArrayList(new Customer("Java"), new Customer("Showyool"), new Customer("Soga")); int index = 0; String[][] exportData = new String[customerList.size()][2]; for (Customer customer : customerList) { exportData[index][0] = String.valueOf(index); exportData[index][1] = customer.getName(); index = index++; } System.out.println(JSON.toJSONString(exportData)); }}class Customer { public Customer(String name) { this.name = name; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; }}复制代码
这段代码看起来好像也没什么的,就是将Customer集合转换成一个字符串二维数组。但是输出结果显示:这就符合我们说的,查询出来有多条,但是输出只有1条。
仔细观察一下,我们可以发现,输出的数据显示都是最后一条,也就是说,Customer这个集合每次遍历的时候,都是后者将前者进行覆盖,也就是说,这个index的下标一直没有变化过,一直是0。
建模
这样看来,我们的这个自增确实有点问题,那么我们再简单来写一个模型
public class Test2 { public static void main(String[] args) { int index = 3; index = index++; System.out.println(index); } }复制代码
我们将上面的业务逻辑简化成这样一个模型,那么这个结果毫无意外的是3。
解释
那么我们执行一下javap,看看JVM字节码是如何解释:
javap -c Test2Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iload_1 3: iinc 1, 1 6: istore_1 7: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 10: iload_1 11: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 14: return}复制代码
这里我简单讲一下这里的JVM字节码指令(后面有机会再详细写写文章)
首先我们需要先知道这里存在两个概念,操作数栈和局部变量表,这两者是存在虚拟机栈当中栈帧(stack frame)当中的一些数据结构,如图:
我们可以简单的理解为,操作数栈的作用是存放数据并且在栈中进行计算数据,而局部变量表则是存放变量的一些信息。
然后我们来看看上面的指令:
0: iconst_3 (先将常量3压入栈)
1: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)
2: iload_1 (将第一个参数的值压入栈,也就是将3入栈,此时栈顶的值为3)
3: iinc 1, 1 (将第一个参数的值进行自增操作,那么此时index的值是4)
6: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)
也就是说index这个参数的值是经历了index->3->4->3,所以这样一轮操作之后,index又回到了一开始赋值的值。
延伸一下
这样一来,我们发现,问题其实出在最后一步,在进行运算之后,又将原先栈中记录的值重新赋给变量,覆盖掉了如果我们这样写:
public class Test2 { public static void main(String[] args) { int index = 3; index++; System.out.println(index); }}Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iinc 1, 1 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: iload_1 9: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 12: return}复制代码
可以发现,这里就没有最后一步的istore_1,那么在iinc之后,index的值就变成我们预想的4。
还有一种情况,我们来看看:
public class Test2 { public static void main(String[] args) { int index = 3; index = index + 2; System.out.println(index); }}Compiled from "Test2.java"public class com.showyool.blog_4.Test2 { public com.showyool.blog_4.Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: iconst_3 1: istore_1 2: iload_1 3: iconst_2 4: iadd 5: istore_1 6: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 9: iload_1 10: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 13: return}复制代码
0: iconst_3 (先将常量3压入栈)
1: istore_1 (出栈操作,将值赋给第一个参数,也就是将3赋值给index)
2: iload_1 (将第一个参数的值压入栈,也就是将3入栈,此时栈顶的值为3)
3: iconst_2 (将常量2压入栈, 此时栈顶的值为2,2在3之上)
4: iadd (将栈顶的两个数进行相加,并将结果压入栈。2+3=5,此时栈顶的值为5)
5: istore_1 (出栈操作,将值赋给第一个参数,也就是将5赋值给index)
看到这里各位观众老爷肯定会有这么一个疑惑,为什么这里的iadd加法操作之后,会影响栈里面的数据,而先前说的iinc不是在栈里面操作?好的吧,我们可以看看JVM虚拟机规范当中,它是这么描述的:
指令iinc对给定的局部变量做自增操作,这条指令是少数几个执行过程中完全不修改操作数栈的指令。它接收两个操作数:第1个局部变量表的位置,第2个位累加数。比如常见的i++,就会产生这条指令
看到这里,我们知道,对于一般的加法操作之后复制没啥问题,但是使用i++之后,那么此时栈顶的数还是之前的旧值,如果此刻进行赋值就会回到原来的旧值,因为它并没有修改栈里面的数据。所以先前那个bug,只需要进行自增不赋值就可以修复了。
最后
感谢各位能够看到这里,以上就是我处理这个bug的全部过程。虽然这只是一个小bug,但是这一个小小的bug还是值得学习和思考的。今后还会继续分享我所发现的bug以及知识点,如果我的文章对你有所帮助,还希望各位大佬点个关注\color{red}{点个关注}点个赞\color{red}{点个赞},再次感谢大家的支持!
相关免费学习推荐:java基础教程