1. 程式人生 > 程式設計 >Java中i++的一些問題總結

Java中i++的一些問題總結

參考內容:

  • 深入理解Java虛擬機器(JVM高階特性與最佳實踐) ——周志明老師
  • 尚矽谷深入理解JVM教學視訊——宋紅康老師

在本文展開前,讀者需要了解一些位元組碼有關的知識,以及JVM虛擬機器棧中棧幀的區域性變量表和運算元棧等知識,筆者在這裡只給出一些大概的簡述。

位元組碼

  • Java位元組碼對於虛擬機器,就好像組合語言對於計算機,屬於基本執行指令。
  • 虛擬機器的指令由一個位元組長度的、代表著某種特定操作含義的數字(稱為操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需引數(稱為運算元,Operands)而構成。由於Java虛擬機器採用面向運算元棧而不是暫存器的結構,所以大多數的指令都不包含運算元,只有一個操作碼。

區域性變量表

區域性變量表:Local Variables,被稱之為區域性變數陣列或本地變量表

定義為一個數字陣列,主要用於儲存方法引數和定義在方法體內的區域性變數,這些資料型別包括各類基本資料型別、物件引用(reference),以及returnAddress型別。

由於區域性變量表是建立線上程的棧上,是執行緒的私有資料,因此不存在資料安全問題。

區域性變量表所需的容量大小是在編譯期確定下來的,並儲存在方法的Code屬性的maximum local variables資料項中。在方法執行期間是不會改變區域性變量表的大小的。

運算元棧

運算元棧:Operand Stack ,使用陣列實現的。

每一個獨立的棧幀除了包含區域性變量表以外,還包含一個後進先出(Last - In - First -Out)的 運算元棧,也可以稱之為 表示式棧(Expression Stack)

運算元棧,在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧(push)和 出棧(pop)

  • 某些位元組碼指令將值壓入運算元棧,其餘的位元組碼指令將運算元取出棧。使用它們後再把結果壓入棧
  • 比如:執行復制、交換、求和等操作
  • 運算元棧,主要用於儲存計算過程的中間結果,同時作為計算過程中變數臨時的儲存空間。

接下來就是本文的正式內容,首先,我們先給出兩個結論:

  • i++與++i在不同情況下可能會有不同的結論;
  • 例項變數/類變數的i++並不是一個原子性的操作。

首先我們看一下i++與++i的解析:

當i++或者++i沒有涉及到其他操作時,兩者是沒有區別的。

// i++
public void method1(){
 int i = 10;
 i++;
}
// ++i
public void method2(){
 int i = 10;
 ++i;
}

對應的位元組碼指令操作為:

// method1
0 bipush 10 // 將10這個整數壓入運算元棧
2 istore_1	// 將運算元棧棧頂元素儲存到區域性變量表中索引為1處
3 iinc 1 by 1 // 區域性變量表中索引為1處的元素,也就是i進行自增(這一步是在區域性變量表上直接進行的,與運算元棧無關)
6 return // 方法返回

// method2
0 bipush 10
2 istore_1
3 iinc 1 by 1 // ++i
6 return

其中關於給出的具體位元組碼細節以及棧幀中運算元棧、區域性變量表在本文開頭給出了一些簡介,具體內容不展開描述,讀者可翻閱與之有關的資料。

通過反編譯可以看出,i++與++i的位元組碼在沒有和其他操作組合時,位元組碼是完全相同的。

當i++或者++i涉及到其他操作時,兩者的位元組碼會有一些改變。

public void method7(){
  int i = 10;
  int a = i++;

  int j = 20;
  int b = ++j;
 }

對應的的位元組碼指令:

 0 bipush 10
 2 istore_1	
 3 iload_1		// i++先從區域性變量表中讀取i到運算元棧
 4 iinc 1 by 1	// i直接在區域性變量表上進行自增
 7 istore_2		// 將運算元棧上讀取到的i的值賦值給a,也就是10
 8 bipush 20
10 istore_3
11 iinc 3 by 1	// ++j則先在區域性變量表上進行自增
14 iload_3		// 再從區域性變量表中讀取j到運算元棧
15 istore 4		// 將運算元棧上讀取到的j的值賦值給b,也就是21
17 returns

通過反編譯可以看出,i++與++i的位元組碼在沒有和其他操作組合時,i++是先取值再自增,而++i是先自增再取值。

還有一個關於i=i++的解析:

public void method8(){
  int i = 10;
  i = i++;
  System.out.println(i);//10
 }

對應的位元組碼指令:

 0 bipush 10
 2 istore_1
 3 iload_1		// 從區域性變量表中讀取i到運算元棧
 4 iinc 1 by 1	// i直接在區域性變量表上進行自增,此時i = 11
 7 istore_1		// 將之前運算元棧上讀取到的i的值賦值給i,之前自增的值被覆蓋了,i = 10
 8 getstatic #2 <java/lang/System.out>
11 iload_1
12 invokevirtual #5 <java/io/PrintStream.println>
15 return

然後看一下例項變數i++這行程式碼的對應的位元組碼

首先我們定義一個類

/**
 * @author XiaoLe
 * @create 2020-12-04 21:15
 * @description
 */
public class Test {
 private int i = 0;
 public void test(){
  i++;
 }
}

通過反編譯檢視test方法中的位元組碼:

 0 aload_0	
 1 dup
 2 getfield #2 <day001/Test.i> // 獲取到Test的i變數的值
 5 iconst_1	// 將int型別常量1壓入棧
 6 iadd	// 棧頂兩個元素相加後返回值入棧
 7 putfield #2 <day001/Test.i>
10 return

可以看到,i++這行程式碼被拆分為三個位元組碼,所以在一些併發情況下,i++如果不做同步處理,就可能會出現資料非一致性。

到此這篇關於Java中i++的一些問題總結的文章就介紹到這了,更多相關Java中i++問題內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!