美文网首页Java
终于修复好了,这次i++引发的bug我希望大家都不要碰到了

终于修复好了,这次i++引发的bug我希望大家都不要碰到了

作者: 该用户已秃头 | 来源:发表于2020-10-21 16:59 被阅读0次

大家好,作为日常写bug修bug的我,今天给大家带来前几天刚刚修复的一个事故。不得不承认,有我的地方总是会有这么多bug。

起因

故事的开始发生在前几天,有一个不是很常用的导出功能,被用户反馈出,不管条件是怎么样,导出的数据只有一条,但是实际上根据条件查询是有很多数据,而且页面中也查询出很多数据。(这个问题已经被修复了,所以当时的Kibana日志也找不到了)于是放下手上的工作,投入其中来看这个问题。

分析

从问题的描述中来分析,那么只可能出现在以下情况:

根据搜索条件查询出来的记录只有一条。

对查询出来的数据进行相关业务处理,导致最后的结果只有一条。

文件导出组件的逻辑处理之后,导致结果只有一条。

题外话 

写到了这里,突然想到了一个经典面试题,MQ消息丢失的场景原因分析。哈哈哈,其实大致上也是这么几个角度分析。(有机会来写MQ的文章)

于是就一个个来分析:

找到相关业务的SQL以及对应的参数,查询可得,数据不止1条,所以第一种情况可以排除。

中间业务当中有涉及到相关权限、数据敏感等,将这些都放开之后,还是只有1条数据。

文件导出组件在接收到数据的时候,打印出的日志也显示只有一条,那么可以说明肯定中间相关业务的逻辑发生了问题。

由于这段代码都是写在一整个方法里面,导致Arthas排查起来就比较困难,只好一步步设置日志进行排查。(所以,如果是一大段逻辑,建议是拆分成duoge 的方法,一来在写的时候思路明确,有一种模块化的概念,至于方法复用什么的我就不多提了,基本操作;二是一但发生问题,排查起来也会方便点,经验之谈)。

最终定位到一个for循环里面。

代码

话不多说,我们直接来看代码。众所周知,我向来是一个很保护公司代码的人,所以,我在这里又不得不给大家模拟一下了。从问题的情况来看,是导出的对象记录是空

importcom.google.common.collect.Lists;

importjava.util.List;

publicclassTest {

    publicstaticvoidmain(String[] args) {

        // 获取Customer数据,这里就简单模拟

        List customerList = Lists.newArrayList(newCustomer("Java"),newCustomer("Showyool"),newCustomer("Soga"));

        int index =0;

        String[][] exportData =newString[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));   

        }

    }classCustomer {

    publicCustomer(Stringname) {

        this.name = name;   

    }

    privateStringname;

    publicStringgetName() {

        returnname;   

    }

    publicvoidsetName(Stringname) {

        this.name = name;   

    }

}

ps:简书对代码格式不太友好,修改的好累~~~

这段代码看起来好像也没什么的,就是将Customer集合转换成一个字符串二维数组。但是输出结果显示:

这就符合我们说的,查询出来有多条,但是输出只有1条。 

仔细观察一下,我们可以发现,输出的数据显示都是最后一条,也就是说,Customer这个集合每次遍历的时候,都是后者将前者进行覆盖,也就是说,这个index的下标一直没有变化过,一直是0。

建模

这样看来,我们的这个自增确实有点问题,那么我们再简单来写一个模型

publicclassTest2{

    publicstaticvoidmain(String[] args){

        intindex =3;       

        index = index++;       

        System.out.println(index);   

    }   

}

我们将上面的业务逻辑简化成这样一个模型,那么这个结果毫无意外的是3。

解释

那么我们执行一下javap,看看JVM字节码是如何解释:

代码复制过来就是这么一坨,我太难了!!!

javap-c Test2

Compiledfrom "Test2.java"

publicclass com.showyool.blog_4.Test2 {

    publiccom.showyool.blog_4.Test2();

        Code:

            0:aload_0

            1:invokespecial #1                  // Method

java/lang/Object."<init>":()V

            4:return

    publicstatic 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以及知识点,如果我的文章对你有所帮助,还希望各位点个关注再次感谢大家的支持!

作者:showyool

链接:https://juejin.im/post/6885187698420613127

来源:掘金

相关文章

  • 终于修复好了,这次i++引发的bug我希望大家都不要碰到了

    大家好,作为日常写bug修bug的我,今天给大家带来前几天刚刚修复的一个事故。不得不承认,有我的地方总是会有这么多...

  • 冒烟测试和回归测试

    何为冒烟测试 冒烟测试在测试中发现问题,找到了一个bug,然后开发人员会来修复这个bug。这时想知道这次修复是否真...

  • Xamarin.Forms学习历程(二)

    哈哈哈~~我最近在写的Xamarin.Forms工程终于到了收尾阶段了,主体功能都实现了,修复一些小Bug以及添加...

  • 前端开发怎么Debug?

    你是否发现:有时候,当某个BUG被我们修复之后,却又发现一个由该BUG引发的另一个BUG,或则由于修复算法的缺陷引...

  • 安卓热修复-multidex的使用

    当程序出现一些小Bug需要紧急修复的时候,又不希望用户感知,可以使用热修复的方式快速修复Bug。 原理:实现多分包...

  • 9个因Bug引发的巨大灾难

    对于程序员来讲,bug就是他们的致命天敌。对于一般的bug来讲,进行简单地修复就解决问题。但以下9个bug,引发了...

  • 为了看你一眼,我绕遍了整座校园(4)

    “好了,好了,我知道了。”汪曾笑的都合不拢嘴,连忙挥手点头。 “我叫汪曾(zeng),希望大家以后不要念错。嗯,现...

  • JS实现图片的不间断连续滚动

    bug修复版2017.07.11 js替代marquee实现图片无缝滚动可能大家都碰到过,当marquee中滚动的...

  • 这次终于击退了bug

    终于,清明那段时间困扰我的bug被击退了,这次终于没有被bug击退。其实引起问题发生的原因很简单,多了两行多余代码...

  • 希望呀,这辈子,都不要结婚,不要碰男的 男人的嘴,骗人的鬼,我是真见识到了

网友评论

    本文标题:终于修复好了,这次i++引发的bug我希望大家都不要碰到了

    本文链接:https://www.haomeiwen.com/subject/wwremktx.html