1. 程式人生 > >【搞定Java基礎】之 i++ 和 ++i 詳解

【搞定Java基礎】之 i++ 和 ++i 詳解

目  錄:

一、i++ 和 ++i 的基本概念

二、i++ 和 ++i 的實現原理

三、i++ 和 ++i 在使用時的一些坑

3.1、i = i++ 導致的結果“異常”

3.2、多執行緒併發引發的混亂


一、i++ 和 ++i 的基本概念

在幾乎所有的指令式程式設計語言中,必然都會有 i++ 和 ++i 這種語法。有些語言中 i++ 和 ++i 既可以作為左值又可以作為右值,筆者專門測試了一下,在Java語言中,這兩條語句都只能作為右值,而不能作為左值。同時,它們都可以作為獨立的一條指令執行

int i = 0;
int j1 = i++; // 正確
int j2 = ++i; // 正確
i++;          // 正確
++i;          // 正確

i++ = 5;      // 編譯不通過
++i = 5;      // 編譯不通過

如下圖所示,當 i++ 或者 ++i 作為左值時,編譯提醒:左邊必須為變數。

關於i++和++i的區別,稍微有經驗的程式設計師都或多或少都是瞭解的,為了文章的完整性,本文也通過例項來簡單地解釋一下。

public static void main(String[] args) {
		
    int i = 1;
    int j1 = i++;
    System.out.println("j1 = " + j1);  // 1
    System.out.println("i = " + i);    // 2
}

執行結果:

public static void main(String[] args) {
		
    int i = 1;
    int j2 = ++i;
    System.out.println("j1 = " + j2);  // 2
    System.out.println("i = " + i);    // 2
}

執行結果:

上面的例子中可以看到,無論是 i++ 和 ++i 指令,對於 變數本身來說是沒有任何區別的,指令執行的結果都是 i 變數的值加 1。而對於 j1 和 j2 來說,這就是區別所在。

int i = 1;
int j1 = i++; // 先將i的原始值(1)賦值給變數j1(1),然後i變數的值加1
int j1 = ++i; // 先將i變數的值加1,然後將i的當前值(2)賦值給變數j1(2)

上面的內容是程式設計基礎,是程式設計師必須要掌握的知識點。本文將在此基礎上更加深入地研究其實現原理和陷阱,也有一定的深度。在讀本文之前,您應該瞭解:

  1. 多執行緒相關知識
  2. Java編譯相關知識
  3. JMM(Java記憶體模型)

本文接下來的主要內容包括:

  1. Java中 i++ 和 ++i 的實現原理
  2. 在使用 i++ 和 ++i 時可能會遇到的一些“坑”

二、i++ 和 ++i 的實現原理

接下來讓我們深入到編譯後的位元組碼層面上來了解 i++ 和 ++i 的實現原理,為了方便對比,筆者將這兩個指令分別放在2個不同的方法中執行,原始碼如下:

public class Test {

    public void testIPlus() {
        int i = 0;
        int j = i++;
    }

    public void testPlusI() {
        int i = 0;
        int j = ++i;
    }
}

將上面的原始碼編譯之後,使用javap命令檢視編譯生成的程式碼(忽略次要程式碼)如下:

...
{
  ... 

  public void testIPlus();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0               // 生成整數0
         1: istore_1               // 將整數0賦值給1號儲存單元(即變數i)
         2: iload_1                // 將1號儲存單元的值載入到資料棧(此時 i=0,棧頂值為0)
         3: iinc          1, 1     // 1號儲存單元的值+1(此時 i=1)
         6: istore_2               // 將資料棧頂的值(0)取出來賦值給2號儲存單元(即變數j,此時i=1,j=0)
         7: return                 // 返回時:i=1,j=0
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 7

  public void testPlusI();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_0                // 生成整數0
         1: istore_1                // 將整數0賦值給1號儲存單元(即變數i)
         2: iinc          1, 1      // 1號儲存單元的值+1(此時 i=1)
         5: iload_1                 // 將1號儲存單元的值載入到資料棧(此時 i=1,棧頂值為1)
         6: istore_2                // 將資料棧頂的值(1)取出來賦值給2號儲存單元(即變數j,此時i=1,j=1)
         7: return                  // 返回時:i=1,j=1
      LineNumberTable:
        line 9: 0
        line 10: 2
        line 11: 7
}
...

可以從上面的位元組碼檔案看出,造成結果不同的原因就是:“1號儲存單元的值加1的操作”和“將1號儲存單元的值載入到資料棧”的先後順序造成的。如果前者在後者之前,則結果就是1,反之則為0。


三、i++ 和 ++i 在使用時的一些坑

i++ 和 ++i 在一些特殊場景下可能會產生意想不到的結果,本節介紹兩種會導致結果混亂的使用場景,並剖析其原因。

3.1、i = i++ 導致的結果“異常”

【案例1:】首先來看一下下面程式碼執行後的結果。

public static void main(String[] args) {
		
    int i = 0;
    i = i++;
    System.out.println("i = " + i);   // 0
}

執行結果:

正常來講,執行的結果應該是i = 1,實際結果卻是:i = 0,這多少會讓人有些詫異。為什麼會出現這種情況呢?我們來從編碼後的程式碼中找答案。上面的程式碼編譯後的核心程式碼如下:

0: iconst_0                          // 生成整數0
1: istore_1                          // 將整數0賦值給1號儲存單元(即變數i,i=0)
2: iload_1                           // 將1號儲存單元的值載入到資料棧(此時 i=0,棧頂值為0)
3: iinc          1, 1                // 1號儲存單元的值+1(此時 i=1)
6: istore_1                          // 將資料棧頂的值(0)取出來賦值給1號儲存單元(即變數i,此時i=0)
7: getstatic      #16                // 下面是列印到控制檯指令
10: new           #22               
13: dup
14: ldc           #24                 
16: invokespecial #26                 
19: iload_1
20: invokevirtual #29                
23: invokevirtual #33                 
26: invokevirtual #37                 
29: return

從編碼指令可以看出,i 被棧頂值所覆蓋,導致最終 的值仍然是 的初始值。無論重複多少次 i = i++ 操作,最終 i 的值都是其初始值。

實際上:i++ 有中間快取變數,,i = i++ 等價於

temp = i;
i = i + 1;
i = temp;

所以 i 不變, 依然是0。

【案例2:】下面用上面這個例子來對比下這個案例:

和上面上面的兩端程式碼中唯一的差別就是 i++ 的結果有沒有賦值給 i ,但是輸出的 i 的結果一個加了1,而1個沒有加。這是為什麼呢?

public static void main(String[] args) {
		
    int i = 0;
    i++;
    System.out.println("i = " + i);   // 1
}

執行結果:

我們看下編譯的位元組碼檔案:

0: iconst_0              // 生成整數0                
1: istore_1              // 將整數0賦值給1號儲存單元(即變數i,i=0)
2: iinc          1, 1    // 1號儲存單元的值+1(此時 i=1)
5: getstatic     #2                  
8: new           #3                  
11: dup
12: invokespecial #4                  
15: ldc           #5                  
17: invokevirtual #6                  
20: iload_1
21: invokevirtual #7                  
24: invokevirtual #8                  
27: invokevirtual #9                  
30: return

【案例3:】i++會產生這樣的結果,那麼++i又會是怎樣呢?同樣的程式碼順序,將i++替換成++i如下:

public static void main(String[] args) {
		
    int i = 0;
    i = ++i;  // IDE丟擲【The assignment to variable i has no effect】警告
    System.out.println("i = " + i);   // 1
}

 IDE丟擲【The assignment to variable i has no effect】警告:

執行結果:

可以看到,使用 ++i 時出現了“正確”的結果,同時Eclipse IDE中丟擲【The assignment to variable i has no effect】警告,警告的意思是將值賦給變數 i 毫無作用,並不會改變 i 的值。也就是說:i = ++i 等價於 ++i

3.2、多執行緒併發引發的混亂

先來看看之前部落格中的一個例子,例子中展示了在多執行緒環境下由 i++ 操作引起的資料混亂。引發混亂的原因是:i++操作不是原子操作

雖然在Java中 i++ 是一條語句,位元組碼層面上也是對應 iinc 這條JVM指令,但是從最底層的CPU層面上來說,i++ 操作大致可以分解為以下3個指令:

1、取數     int  temp = i;

2、累加     i = i + 1;

3、儲存     i = temp; 

其中的一條指令可以保證是原子操作,但是3條指令合在一起卻不是,這就導致了 i++ 語句不是原子操作。

如果變數 i 用 volatile 修飾是否可以保證 i++ 是原子操作呢,實際上這也是不行的。因為 volatile 不能保證變數狀態的“原子性操作”。如果要保證累加操作的原子性,可以採取下面的方法:

1、將 i++ 置於同步塊中,可以是synchronized或者J.U.C中的排他鎖(如ReentrantLock等)。

2、使用原子性(Atomic)類替換 i++,具體使用哪個類由變數型別決定。如果 i 是整形,則使用 AtomicInteger 類,其中的 AtomicInteger.getAndIncrement() 就對應著 i++ 語句,不過它是原子性操作。

本文轉發自:http://hinylover.space/2017/07/30/java-i-self-increament/