i++ 的線程安全性和改進

一、i++ 是線程安全的嗎?

1、測試環境

  • 使用jdk8
  • 使用Executors創建線程池
  • 線程池容量100執行1000次自增操作

2、代碼驗證

(1)全部代碼

public class UnsafeDemo {

    private int cnt = 0;

    public void UnSafeAdd() {
        cnt++;
    }

    public int getCnt() {
        return cnt;
    }

    public static void main(String[] args) {
        int exePoolSize = 100;
        int forSize = 1000;
        UnsafeDemo unsafeDemo = new UnsafeDemo();
        ExecutorService exe = Executors.newFixedThreadPool(exePoolSize);
        CountDownLatch countDownLatch = new CountDownLatch(forSize);
        for (int i = 0; i < forSize; i++) {
            Runnable runnable = () -> {
                unsafeDemo.UnSafeAdd();
                countDownLatch.countDown();
            };
            exe.execute(runnable);
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        exe.shutdown();
        System.out.println(unsafeDemo.getCnt());
    }

}

(2)相關說明

  • 線程池使用Executors.newFixedThreadPool(exePoolSize)創建,創建一個可複用的固定的線程池,exePoolSize表示線程池的容量大小。
  • 使用CountDownLatch倒數器,保證各個線程執行完自增操作再執行輸出操作。
  • 通過上面代碼,期望的輸出值是1000,但是多次測試,輸出結果都是小於1000.

(3)結論

由於內存不可見的原因,如果一個線程運算完後還沒刷到主內存,比如從0加到1,此時主內存仍爲0,而另一個線程讀取到0然後從0加到1,所以當他們執行完將數據刷到主內存,執行了兩次從0到1的操作,最終結果是1。

二、線程安全的自增操作

1、測試環境

  • 使用jdk8
  • 使用ThreadPoolExecutor創建線程池
  • 使用AtomicInteger執行自增操作

2、代碼驗證

(1)全部代碼

public class SafeDemo {

    private static int COREPOOLSIZE = 5;
    private static int MAXIMUMPOOLSIZE = 200;
    private static long KEEPALIVETIME = 0L;
    private static int QUESIZE = 100;
    // 拒絕策略
    // ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常, 默認策略
    // ThreadPoolExecutor.DiscardPolicy:丟棄任務不拋出異常。 
    // ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程) 
    // ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
    private static ThreadPoolExecutor.CallerRunsPolicy rejectHandler = new ThreadPoolExecutor.CallerRunsPolicy();
    // 儘量不使用Executors創建線程池,Executors有可能導致OOM發生
    public static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(COREPOOLSIZE, MAXIMUMPOOLSIZE, KEEPALIVETIME, TimeUnit.SECONDS, new LinkedBlockingDeque<>(QUESIZE), Thread::new, rejectHandler);

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger cnt = new AtomicInteger();
        int forSize = 100000;
        CountDownLatch countDownLatch = new CountDownLatch(forSize);
        for (int i = 0; i < forSize; i++) {
            Runnable runnable = () -> {
                cnt.incrementAndGet();
                countDownLatch.countDown();
            };
            poolExecutor.execute(runnable);
        }
        countDownLatch.await();
        System.out.println(cnt.get());
    }
}

(2)相關說明

  • 此時使用ThreadPoolExecutor創建線程池,同樣在阿里巴巴代碼規範中也明確說明不能使用Executors創建線程池,防止在未知情況下OOM的發生
  • 使用AtomicInteger,這是JUC包下atomic的其中一個代表類,使用CAS保證可見性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章