慕課網實戰·高併發探索(四):線程安全性-可見性-有序性

可見性

什麼是可見性?

一個線程對主內存的修改可以及時的被其他線程觀察到

導致共享變量在線程間不可見的原因

  • 線程交叉執行
  • 重排序結合線程交叉執行
  • 共享變量更新後的值沒有在工作內存與主存間及時更新

JVM處理可見性

JVM對於可見性,提供了synchronized和volatile

JMM關於synchronized的兩條規定:

  • 線程解鎖前,必須把共享變量的最新值刷新到主內存
  • 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖是同一把鎖

Volatile:通過加入內存屏障禁止重排序優化來實現

  • 對volatile變量寫操作時,會在寫操作後加入一條store屏障指令,將本地內存中的共享變量值刷新到主內存。

這裏寫圖片描述

  • 對volatile變量讀操作時,會在讀操作前加入一條load屏障指令,從主內存中讀取共享變量。

這裏寫圖片描述

  • volatile的屏障操作都是cpu級別的。

  • 適合狀態驗證,不適合累加值,volatile關鍵字不具有原子性
    舉個例子:我們仍用高併發學習(二)中的例子來說明,對一個int型數值的多線程讀寫操作。我們將count變量用volatile來修飾:

public class CountExample {

    //請求總數
    public static int clientTotal  = 5000;
    //同時併發執行的線程數
    public static int threadTotal = 200;
    //計數 *
    public static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        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 (InterruptedException e) {
                    log.error("excption",e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();//保證信號量減爲0
        executorService.shutdown();//關閉線程池
        log.info("count:{}",count);
    }

    private static void add(){
        count++;
    }
}

多次運行代碼我們發現:count的最終結果並不是預期的5000,而是有時爲5000,但是大多數時間比5000小,這是爲什麼呢?原因在於對count++的操作中,jvm對count做了三步操作:

1、從主存中取出count的值放入工作變量 count
2、對工作變量中的count進行+1
3、將工作變量中的count刷新回主存中

在單線程執行此操作絕對沒有問題,但是在多線程環境中,假設有兩個線程A、B同時執行count++操作,某一刻A與B同時讀取主存中count的值,然後在自己線程對應的工作空間中對count+1,最後又同時將count+1的值寫回主存。到此,count+1的值被寫回主存兩遍,所以導致最終的count值小了1。在整體程序執行過程中,該事件發生一次或多次,自然結果就不正確。
那麼volatile適合做什麼呢?其實它比較適合做狀態標記量(不會涉及到多線程同時讀寫的操作),而且要保證兩點:
(1)對變量的寫操作不依賴於當前值
(2)該變量沒有包含在具有其他變量的不變的式子中
例如:

volatile boolean inited = false;
//線程一:
context = loadContext();
inited = true;

//線程二:
while(!inited){
    sleep();
}
doSomethingWithConfig(context);

有序性

什麼是有序性?

Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
關於重排序,詳情見:高併發學習(二)– 4、亂序執行優化

java中保證有序性

java提供了 volatile、synchronized、Lock可以用來保證有序性
另外,java內存模型具備一些先天的有序性,即不需要任何手段就能得到保證的有序性。通常被我們成爲happens-before原則(先行發生原則)。如果兩個線程的執行順序無法從happens-before原則推導出來,那麼就不能保證它們的有序性,虛擬機就可以對它們進行重排序。

【以下規則來自於《深入理解java虛擬機》】

  • 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unlock操作先行發生於後面對同一個鎖的lock操作
  • volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作(重要)
  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
  • ——————————————————————————————————————————————
  • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個動作
  • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  • 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
  • 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章