Java 기본 튜토리얼칼럼에서는 i++로 인해 발생한 버그를 소개합니다.
안녕하세요 여러분, 매일 글을 쓰고 버그를 고치는 사람으로서 오늘은 며칠 전 해결된 사고 소식을 전해드리려고 합니다. 내가 어디에 있든 항상 버그가 너무 많다는 것을 인정해야 합니다.
몇일전부터 잘 사용되지 않는 내보내기 기능이 있었습니다. 조건에 따라 데이터가 많고, 페이지에 쿼리되는 데이터도 많습니다. (이 문제는 해결되어 당시의 Kibana 로그를 더 이상 찾을 수 없습니다.) 그래서 이 문제를 살펴보기 위해 작업을 내려놓고 몰입했습니다.
문제 설명에 따르면 다음과 같은 상황에서만 발생할 수 있습니다.
Digression
이 글을 쓰면서 문득 MQ 메시지 손실 원인 분석이라는 고전적인 인터뷰 질문이 생각났습니다. 하하하 사실 여러 각도에서 대략적으로 분석이 됩니다. (MQ에 대한 글을 쓸 기회가 생겼습니다)
Digression
그래서 하나씩 분석해 봤습니다.
이 코드는 전체 메소드로 작성되었기 때문에 아서스가 문제 해결을 하기가 어려우므로 문제 해결을 위해 로그를 단계별로 설정해야 합니다. (그래서 로직의 양이 크다면 듀오지 서브메소드로 분할하는 것을 추천합니다. 우선 작성시 아이디어가 명확해지고, 모듈화 개념이 있을 것입니다. 메소드 재사용에 관해서는 저는 기본 작업에 대해서는 더 이상 언급하지 않겠습니다. 둘째, 일단 문제가 발생하면 문제 해결이 더 쉬울 것입니다.
마지막으로 for 루프 안에 배치됩니다.
더 이상 고민하지 말고 코드를 직접 살펴보겠습니다. 우리 모두 알고 있듯이 저는 항상 회사의 코드를 보호하는 사람이었으므로 여기 있는 모든 사람을 위해 이를 시뮬레이션해야 합니다. 문제로 판단하면 내보낸 개체 레코드가 비어 있습니다
import com.google.common.collect.Lists;import java.util.List;public class Test { public static void main(String[] args) { // 获取Customer数据,这里就简单模拟 ListcustomerList = 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 컬렉션을 2차원 문자열 배열로 변환하는 것뿐입니다. 그러나 출력 결과는 다음과 같습니다.이것은 우리가 말한 것과 일치합니다. 쿼리가 여러 개 있지만 하나만 출력됩니다.
주의 깊게 살펴보면 출력 데이터가 항상 마지막 데이터임을 알 수 있습니다. 즉, Customer 컬렉션을 탐색할 때마다 후자가 전자를 덮어쓰는 것을 알 수 있습니다. 변경되었으나 항상 0이었습니다.
자체 증분에 문제가 있는 것 같으니 간단하게 모델을 작성해 보겠습니다
public class Test2 { public static void main(String[] args) { int index = 3; index = index++; System.out.println(index); } }复制代码
위의 비즈니스 로직을 이러한 모델로 단순화하면 결과는 놀랄 일이 아닙니다.
그런 다음 javap를 실행하고 JVM 바이트코드가 어떻게 해석되는지 살펴보겠습니다.
javap -c Test2 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."":()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 바이트코드 지침에 대해 간략하게 설명하겠습니다(나중에 기회가 되면 자세히 기사를 작성하겠습니다)
먼저 먼저 여기에 피연산자 스택과 로컬 변수 테이블이라는 두 가지 개념이 있다는 것을 알아야 합니다. 이 두 가지는 그림과 같이 가상 머신 스택의 스택 프레임에 존재하는 일부 데이터 구조입니다.
우리는 간단히 이해하세요. 왜냐하면 피연산자 스택의 기능은 스택에 데이터를 저장하고 데이터를 계산하는 것이고, 지역 변수 테이블은 변수에 대한 일부 정보를 저장하기 때문입니다.
그런 다음 위의 명령을 살펴보겠습니다.
0: icont_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."":()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."":()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,只需要进行自增不赋值就可以修复了。
이 글을 읽어주셔서 감사합니다. 위 내용은 제가 이 버그를 처리하는 전체 과정입니다. 이것은 단지 작은 버그일 뿐이지만, 이 작은 버그는 여전히 배우고 생각해 볼 가치가 있습니다. 앞으로도 제가 발견한 버그와 지식포인트를 공유하겠습니다. 제 글이 도움이 되셨다면 팔로우도 부탁드려요 click좋아요
위 내용은 i++로 인한 버그 해결의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!