線程安全-可見性

共享變量在線程間不可見的緣由
  • 線程的交叉執行
  • 重排序結合線程交叉執行
  • 共享變量更新後的值沒有在工做內存與主內存間及時更新
  1. 使用synchronized的來保證可見性

使用synchronized的兩條規定:java

  • 線程解鎖前,必須把共享變量的最新值刷新到主內存
  • 線程加鎖鎖時,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新的值(注意加鎖與解鎖是同一把鎖)
  1. volatile 來實現可見性
    經過加入內存屏障和禁止重拍訊優化來實現可見性。
  • 對volatile變量寫操做時,會在寫操做後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存
  • 對volatile變量進行讀操做時,會在讀操做前加入一條load屏障指令,從主內存中讀取共享變量。
也就是說使用volatile關鍵字在讀和寫操做時都會強迫從主內存中獲取變量值。

下圖是使用volatile寫操做的示意圖併發

使用volatile寫操做前會插入一條StoreStore指令來禁止在volatile寫以前的普通寫對volatile寫的指令重排序優化,在寫以後會插入一條StoreLoad屏障指令來防止上面的volatile寫操做和下面可能有的讀或者寫進行指令重排序。

下圖是volatile讀操做示意圖優化

  • volatile操做都是cpu指令級別的

下面看一段演示代碼ui

@Slf4j
public class CountExample4 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時併發執行的線程數
    public static int threadTotal = 200;

    public static volatile int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
        // 一、count
        // 二、+1
        // 三、count
    }
}

咱們屢次運行個這段代碼,發現結果並非咱們預期5000,volatile只能保證可見性並不能保證原子性。spa

一般來講使用volatile須要具有兩個條件線程

  • 對變量寫操做不依賴當前值
  • 該變量沒有包含在其餘變量的所在的式中

因此volatile很是適合用做狀態標記量,好比作爲線程是否被初始化。還有就是用double check 我以前的博客就提到的單例模式中就使用了volatile來作double check 雙重檢查實現單例。
code

相關文章
相關標籤/搜索