1. 程式人生 > 實用技巧 >修改el-form表單的el-form-item的label的字型大小以及修改el-row中的el-col的高度

修改el-form表單的el-form-item的label的字型大小以及修改el-row中的el-col的高度

概述

Java記憶體模型是遮蔽掉硬體和作業系統記憶體訪問差異,實現在各個平臺記憶體訪問的一致性。本文就介紹一下Java記憶體模型原理,之後介紹一下併發程式設計中常見的原子性、可見性、有序性問題。

主記憶體和工作記憶體

由於記憶體和CPU效能的差異,所以現代計算機都使用多級快取的方式來加快運算速度,也就是說CPU不能直接操作記憶體,而是需要先把記憶體中的資料拷貝到快取記憶體或者暫存器才可以修改。Java中的公共變數是儲存在主記憶體中,而區域性變數和方法的引數不是儲存在主記憶體中,而是儲存在各個執行緒自己的工作棧中,下面就介紹一下Java的工作記憶體和主記憶體是如何互動的。

工作記憶體和主記憶體互動

         圖片來源:一篇文章搞懂Java記憶體模型(詳解)

  • lock(鎖定):作用於主記憶體的變數,一個變數在同一時間只能一個執行緒鎖定,該操作表示這條線成獨佔這個變數
  • unlock(解鎖):作用於主記憶體的變數,表示這個變數的狀態由處於鎖定狀態被釋放,這樣其他執行緒才能對該變數進行鎖定
  • read(讀取):作用於主記憶體變數,表示把一個主記憶體變數的值傳輸到執行緒的工作記憶體,以便隨後的load操作使用
  • load(載入):作用於執行緒的工作記憶體的變數,表示把read操作從主記憶體中讀取的變數的值放到工作記憶體的變數副本中(副本是相對於主記憶體的變數而言的)
  • use(使用):
    作用於執行緒的工作記憶體中的變數,表示把工作記憶體中的一個變數的值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時就會執行該操作
  • assign(賦值):作用於執行緒的工作記憶體的變數,表示把執行引擎返回的結果賦值給工作記憶體中的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時就會執行該操作
  • store(儲存):作用於執行緒的工作記憶體中的變數,把工作記憶體中的一個變數的值傳遞給主記憶體,以便隨後的write操作使用
  • write(寫入):作用於主記憶體的變數,把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中

問題

硬體結構決定了Java記憶體模型的形式,而這套結構會導致很多問題,主要就是原子性、有序性、可見性等問題。

原子性

定義:一個操作執行過程中不能被打斷,要麼全部執行成功,要麼全部失敗。java可以保證簡單的賦值操作是原子性的,比如int i = 1;在32虛擬機器上無法保證long和double的賦值操作是原子性,對於32位作業系統來說,單次次操作能處理的最長長度為32bit,而long型別8位元組64bit,所以對long的讀寫都要兩條指令才能完成(即每次讀寫64bit中的32bit)。

i++

上面的操作同樣無法保證原子性,因為上面的操作其實是分了3步

  • 獲取i的值
  • 將i的值加1
  • 將結果賦值給i

以上三步操作不是一個原子操作,中間可以被打斷。

可見性

所謂可見性,就是一個處理器修改了某個變數的值,別的處理器立即可以獲得這個修改資訊。

圖中有兩個處理器,假設處理器0修改了主記憶體中的某個變數的副本,然後將結果放到寫緩衝器,暫存器,快取記憶體等地方,對於處理器1來說都是不可見的,那如果處理器1要想要可見,處理器0在修改完之後一定要通知通知處理器1,讓處理器1的變數副本失效,同時要將自己的結果同步到主記憶體中。

具體的解決辦法就是使用記憶體屏障,比如處理器0修改了變數的副本,然後執行store屏障,這個時候處理器0需要執行flush操作,把資料寫回到主記憶體,同時通知處理器1,讓其中的變數副本失效,使用記憶體屏障確實可以解決可見性的問題,但是相應的程式的執行效率會降低。

有序性

所謂有序性,就是在本執行緒內觀察,所有操作都是有序的。在一個執行緒觀察另一個執行緒,所有操作都是無序的,無序是因為發生了指令重排序

上圖中給出了會發生重排序的地方,下面就逐一介紹一下。

  • 編譯的時候,編譯器會重排指令
  • 處理器也會重排指令,處理器執行的順序可能並不是編譯之後的順序
  • 硬體層間也會導致有序性問題,比如前一個指令執行的結果放入到寫緩衝器中還沒有同步到快取記憶體,後一個指令的結果卻已經放入到快取記憶體中

解決有序性的問題還是依靠記憶體屏障,具體參考這篇文章

指令重排,並不是亂排的,而是要遵循一定的規則,這個規則就是java提前已經定義好happens-before 原則,下面簡單介紹一下。

happens-before 原則

  • 程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作
  • 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作
  • volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作
  • 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
  • 執行緒啟動規則:Thread物件的start()方法先行發生於此執行緒的每個一個動作
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生
  • 執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到執行緒已經終止執行
  • 物件終結規則:一個物件的初始化完成先行發生於他的finalize()方法的開始

這8條規則中,前4條規則是比較重要的,後4條規則都是顯而易見的。

  下面我們來解釋一下前4條規則:

  對於程式次序規則來說,我的理解就是一段程式程式碼的執行在單個執行緒中看起來是有序的。注意,雖然這條規則中提到“書寫在前面的操作先行發生於書寫在後面的操作”,這個應該是程式看起來執行的順序是按照程式碼順序執行的,因為虛擬機器可能會對程式程式碼進行指令重排序。雖然進行重排序,但是最終執行的結果是與程式順序執行的結果一致的,它只會對不存在資料依賴性的指令進行重排序。因此,在單個執行緒中,程式執行看起來是有序執行的,這一點要注意理解。事實上,這個規則是用來保證程式在單執行緒中執行結果的正確性,但無法保證程式在多執行緒中執行的正確性。

  第二條規則也比較容易理解,也就是說無論在單執行緒中還是多執行緒中,同一個鎖如果出於被鎖定的狀態,那麼必須先對鎖進行了釋放操作,後面才能繼續進行lock操作。

  第三條規則是一條比較重要的規則,也是後文將要重點講述的內容。直觀地解釋就是,如果一個執行緒先去寫一個變數,然後一個執行緒去進行讀取,那麼寫入操作肯定會先行發生於讀操作。

  第四條規則實際上就是體現happens-before原則具備傳遞性。

例子

有序性問題有一個很著名的例子就是單例模式使用double check在高併發的時候依然可能會出問題,具體如下。

public class Singleton {
    private Singleton() { }
    private volatile static Singleton instance;
    public Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  物件的建立要經過如下三步

memory = allocate();  // 1:分配物件的記憶體空間
ctorInstance(memory); // 2:初始化物件
instance = memory;  // 3:設定instance指向剛分配的記憶體地址

  如果第二步和第三步發生了指令重排,先執行了第三步,執行完之後物件就不為null了,如果另一個執行緒以為單例模式已經初始化完成了,就開始使用這個物件,這時由於物件還沒有初始化完成,就會出問題。所以解決的辦法就是對單例物件加上volatile,加了volatile之後,相當於如下:

public class Singleton {
    private Singleton() { }
    private volatile static Singleton instance;
    public Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                  LoadStore();//虛擬碼
                    StoreStore();//虛擬碼
                    instance = new Singleton();
                  Store();//虛擬碼
                }
            }
        }
        return instance;
    }
} 

在instance前面加入了兩個記憶體屏障,第一個記憶體屏障LoadStore在別的執行緒執行如下程式碼的時候:

   if(instance==null){

  由於有LoadStore屏障的存在,這個Load操作要等到Store操作完成,也就是這個屏障前面的所有Store操作都完成,但是加這個屏障並不能保證例項化物件的時候,那三步的指令重排,也就是說,單例的建立依然有可能先執行第三步分配地址空間,後執行第二部初始化物件,但這個指令重排沒有影響,有了屏障之後會等待全部執行完成。

上面的Store屏障(這個說的有些混亂,因為屏障這個東西很多不同的硬體廠家實現都不一樣,理解原理就可以了),這個屏障的作用是當初始化完成會把instance強制重新整理回主記憶體。

總結

  本文主要介紹Java記憶體模型結構,主記憶體和工作記憶體的互動方式,之後介紹在併發程式設計中幾個重要的性質,分別是原子性、有序性、可見性,介紹了在什麼場景下會出現問題,之後介紹了一個單例模式例項化的例子,用於說明有序性可能會出現的問題以及解決的辦法。

參考:

單例模式+volatile禁止指令重排序

併發之原子性、可見性、有序性

一篇文章搞懂Java記憶體模型(詳解)

深入理解Java記憶體模型