在併發編程中我們常說的“競態”是什麼?

1 何謂“競態”

之前在學習一篇文章的時候,就看到“競態”,但是不知道什麼意思,文章中也沒有對“競態”做更多的解釋,後來經過一番的探索,終於弄的差不多明白了,今天寫點總結。
首先,我們要明白“競態”是什麼。先說我的結論吧,“競態”就是在多線程的編程中,你在同一段代碼裏輸入了相同的條件,但是會輸出不確定的結果的情況。我不知道這個解釋是不是夠清楚,我們接着往下看,下面我們用一段代碼來解釋一下啊。
出現競態條件的代碼:

public class MineRaceConditionDemo {
    private int sharedValue = 0;
    private final static int MAX = 1000;
    private int raceCondition() {
        if (sharedValue < MAX) {
            sharedValue++;
        } else {
            sharedValue = 0;
        }
        return sharedValue;
    }

    public static void main(String[] args) {
        MineRaceConditionDemo m = new MineRaceConditionDemo();
        ExecutorService es = new ThreadPoolExecutor(10,
                10,
                5,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(1000),
                new ThreadPoolExecutor.CallerRunsPolicy());
        ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 1000; i++) {
            es.execute(() -> {
                try {
                //  這是精髓所在啊,如果沒有這個,那麼要跑好幾次纔會出現競態條件。
                //  這個用來模擬程序中別的代碼的處理時間。
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int num = m.raceCondition();
                if (map.get(num) != null) {
                    System.out.println("the repeat num: " + num);
                    System.out.println("happen.");
                } else {
                    map.put(num, 0);
                }
            });
        }
        es.shutdown();
    }
}

以上的代碼是我自己的設計的一段會出現競態條件的代碼,比較簡陋,但是可以說明問題了,你只要運行上面這段代碼,每次的輸出的結果級大概率都是不同的,但是也有以外,比如你的電腦的性能很強,這段代碼也會出現執行正確的情況,也就是啥也不輸出。
比如有的時候輸出這個:

the repeat num: 78
happen.
the repeat num: 229
happen.
the repeat num: 267
happen.
the repeat num: 267
happen.
the repeat num: 498
happen.

有點時候輸出這個:

the repeat num: 25
happen.
the repeat num: 157
happen.

當然,以上的是我的輸出的,你們的輸出肯定也是不同的。
對於上面這些,同一段代碼,對於同樣的輸出,但是程序的輸出有的時候是正確,有的時候是錯誤的,這種情況,我們稱之爲“競態”。最要命的就是,代碼每次輸出不是每次都錯誤,而是你不知道他什麼時候會正確,什麼時候會錯誤。
當然,如果以上的代碼執行的情況就是,啥都不輸出,所有的值都是唯一的。

2 “競態”爲什麼會發生?

“競態”的發生主要是因爲多個線程都對一個共享變量(比如上面的 sharedValue 就屬於共享變量)有讀取-修改的操作。在某個線程讀取共享變量之後,進行相關操作的時候,別的線程把這個變量給改了,從而導致結果出現了錯誤。

什麼樣的代碼模式會發生“競態”

這部分知識主要是來自《Java多線程編程實戰指南 核心篇》。
這裏書中提到,會發生競態條件就是兩個模式:read-modify-write(讀-改-寫)和 check-than-act(檢測而後行動)。
當然,這裏面的都有一個相同的操作過程:某個有讀取這個“共享變量”的操作,然後別的線程有個修改這個變量的操作。這裏有個重點,在多個線程中,起碼有一個線程有更新操作;如果所有的線程都是讀操作,那麼就不存在什麼競態條件。
總體來說,就是要thread1#load - thread2#update。 這種的模式,起碼是是要有兩個線程的,而且其中某個線程肯定是要有更新“共享變量”操作的,另一個線程不管是讀取變量還是更新變量都會出現錯誤(要麼讀取髒數據、要麼丟失更新結果)。

3 如何消除“競態”?

單以上面的操作來說,一般來說有兩種解法方式,

3.1 加鎖

加上synchronized關鍵字,保證每次只能有一個線程獲取共享變量的使用權。

private synchronized int raceCondition() {
    if (sharedValue < MAX) {
        sharedValue++;
    } else {
        sharedValue = 0;
    }
    return sharedValue;
}

3.2 利用原子操作

利用java的工具包裏的 AtomicInteger,代替int,利用原子操作消除“競態”。

private AtomicInteger sharedValue = new AtomicInteger(0);
private final static int MAX = 1000;
private int raceCondition() {
    if (sharedValue.get() < MAX) {
        return sharedValue.getAndIncrement();
    } else {
        sharedValue.set(0);
        return sharedValue.get();
    }
}

以上兩種方法的要義就是保證每個線程在操作“共享變量”的都是原子操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章