synchronized下的 i+=2 和 i++ i++執行結果居然不一樣

颯沓流星發表於2022-06-12

起因

逛【部落格園-博問】時發現了一段有意思的問題:

問題連結:https://q.cnblogs.com/q/140032/

這段程式碼是這樣的:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AutomicityTest implements Runnable {

    private int i = 0;

    public int getValue() {
        return i;
    }

    /**
    * 同步方法,加2
    */
    public synchronized void evenIncrement() {
        i += 2;
        // i++;
        // i++;
    }

    @Override
    public void run() {
        while (true) {
            evenIncrement();
        }
    }

    public static void main(String[] args) {

        ExecutorService exec = Executors.newCachedThreadPool();

        // AutomicityTest執行緒
        AutomicityTest automicityTest = new AutomicityTest();
        exec.execute(automicityTest);

        // main執行緒
        while (true) {
            int value = automicityTest.getValue();
            if (value % 2 != 0) {
                System.out.println(value);
                System.exit(0);
            }
        }
    }
}

程式碼很簡單(一開始我沒看清楚問題,還建議樓主去學習下Java語法,尷尬的一批~~~)

簡單說明下程式碼邏輯

一、AutomicityTest類實現了Runnable介面,並實現 run() 方法,run() 方法中有一個 while(true) 迴圈,迴圈體中呼叫了一個 synchronized 修飾的方法 evenIncrement();

二、AutomicityTest類中有一個 int i 成員變數;

三、在 main() 方法中使用執行緒池執行執行緒(直接 new Thread().start() 是一樣的),然後 while(true) 迴圈體中不停列印成員變數 i 的值,如果是奇數就退出虛擬機器。

大家覺得這段程式碼會輸出什麼呢?

沒錯就是死迴圈!!!

有意思的地方來了,如果我把同步方法 evenIncrement() 改為下面這樣:

public synchronized void evenIncrement() {
    // i += 2;
    i++;
    i++;
}

執行結果是退出了虛擬機器!!!

是不是很納悶兒,要是不納悶兒,就不用往下看了 >_<

一起來分析一下

1.首先從方法入口開始,main() 方法當中建立了一個AutomicityTest執行緒 和 本身 main() 所在的main執行緒,所以AutomicityTest執行緒是一個寫執行緒,main執行緒是一個讀執行緒,既然有讀有寫,又是多個執行緒,就涉及到工作記憶體和主存模型,在這裡我就不贅述了。

2.寫執行緒是有synchronized修飾的,但是讀執行緒並沒有,這就導致了讀寫不一致,解決方法就是給 getValue() 加上synchronized,此時執行結果就正常了

3.但是問題還沒完,為什麼 i += 2 死迴圈,而 【兩條】 i++ 卻退出了虛擬機器呢?

位元組碼層面分析

public synchronized void evenIncrement() {
    i += 2;
}

// 對應的位元組碼
public synchronized void evenIncrement();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_2
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
public synchronized void evenIncrement() {
    i++;
    i++;
}

// 對應的位元組碼
public synchronized void evenIncrement();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: aload_0
        11: dup
        12: getfield      #2                  // Field i:I
        15: iconst_1
        16: iadd
        17: putfield      #2                  // Field i:I
        20: return

i+=2;

位元組碼指令只有一段putfield,沒有執行是偶數,執行了也是偶數,所以會死迴圈。

i++; i++;

位元組碼指令有兩段putfield,由於之前getValue()沒有加synchronized,那麼在執行getValue()的時候,putfield可能沒有執行,可能執行了一次,也可能執行了兩次,沒有執行是偶數,執行一次是奇數,執行兩次是偶數;又因為AutomicityTest執行緒 run() 是 while (true) {} 的,所以它總能執行到奇數,退出虛擬機器。

最後

到此就分析完了,所以該問題的關鍵就是寫操作是原子的,但是讀操作不是,導致讀出來的資料不是最終的。

相關文章