java中線程安全,volatile,synchronized,鎖,線程同步,鎖的狀態和鎖升級,CAS ABA,happens-before

在說線程之前,首先必須要說的一個概念是進程,任何線程不能獨立存在,進程只操作系統結構的基礎,是代碼在數據集上的一次運行活動,是系統進行資源分配和調度的一個獨立單位,線程可以理解爲是進程的子任務,是進程的一個執行路徑,進程中的多個線程共享進程的資源。我們知道CPU執行是給每個線程分配CPU時間片來執行線程業務,時間片是CPU份額配給各個線程的時間,非常短,CPU通過不停切換線程執行,充分利用CPU和發揮CPU多核。這裏就存在一個問題,CPU在線程之間來回切換執行,切換到之前執行的線程,如何繼續執行下去,所以每次切換之前會給當前線程保存一個狀態,一般說線程上下文,當線程再次獲得CPU時間片執行的時候加載線程上下文繼續之前的執行邏輯,線程上下文的切換會影響多線程的執行速度
在Java中一般要想實現自定義的線程有兩種方式,一種是繼承Thread類,另一種是實現 Runnable接口。
在java中,與線程相關的一個重要話題就是線程安全問題,在說線程安全問題之前,我們先需要了解一下java中的內存模型(Java Mermory Model,JMM)
JMM
在java中線程運行的時候,當需要讀取某個變量的時候,由JMM將變量讀取到本地線程副本,當對變量修改完後,由JMM負責寫回到主內存(不定時,無法確定詳細時間)。現代處理器一般都是將數據寫入到臨時寫緩衝區,寫緩衝區保證指令流水線持續運行,避免CPU等待向內存寫入數據的延遲,以批處理方式刷新寫緩衝區、合併寫緩衝區中對同一地址的多次寫,減少對內存總線的佔用。
需要理解的是,CPU並不直接和主內存打交道,而是速度更快的L1,L2,L3 緩存行,當CPU需要對內存中的數據進行運算的時候,先在緩存行查找是否有這個數據,如果沒有在從主內存中將數據加載到緩存行中進行運算
由於各個平臺,系統以及硬件的不同,硬件級別和系統級別提供的內存操作語義可能不盡相同,爲了對開發者能夠有一個統一的內存操作模型,JMM爲開發者提供了跨平臺、跨硬件、跨系統的統一內存操作模型,提供一致的內存可見性。

何謂線程安全,當多個線程同時讀寫一個共享資源且沒有任何同步措施的時候,導致出現髒數據或其他不預見的結果的問題。
指令重排:實際程序運行的時候,爲了提高性能,編譯器和處理器常常會對指令做重排序,在不影響結果的前提下,提升並行度和程序效率,主要有三種情況:
(1)編譯器優化的重排序,在單線程內不改變程序語義的前提的時,指令重排
(2)指令級並行的重排序。在多核CPU下,採用指令級並行技術,將多條指令並行處理,如果不存在數據依賴,處理器可以改變語句對應機器指令的執行程序
(3)內存系統的重排序。由於緩存和讀寫緩衝區,使得加載和存儲操作看上去可能是在亂序執行。

Java中使用happens-before來描述操作之間的內存可見性,如果一個操作執行的結果需要對另一個操作可見,這兩個操作之間必須存在happens-before關係,happens-before指定了兩個操作之間的執行順序,可以在不同線程之間
happens-before描述如下:

  • 如果A操作happens-before B 操作,那麼A操作的執行結果必須對B操作可見,且A操作執行順序在B操作之前

  • 如果A,B兩個操作存在happens-before規則,並不意味着Java平臺必須嚴格按照happens-before關係順序執行,如果重排序之後的執行結果與按照happens-before執行結果易一致,那麼這種重排序是合法的。
    一般常見happends-before規則如下:

  • 程序順序規則,一個線程中的每個操作,happens-before於該線程中任意後續操作

  • 鎖規則,對一個鎖的解鎖,happens-before於隨後這個鎖的加鎖

  • volatile規則,對volatile變量的寫happens-before於後續對這個變量的讀

  • 傳遞性,如果 A happens-before B,B happens before C,則 A happens before C

  • start規則,如果ThreadA執行 ThreadB.start,則ThreadA的ThreadB.start操作happens-before線程B中的任意操作

  • join規則,如果線程A執行ThreadB.join,則線程B中任意操作happens-before線程A的ThreadB.join操作

在以下幾種情況下,不會重排序:

  • 數據存在依賴情況下,如果兩個操作訪問同一個變量,且其中有一個爲寫操作,不會重排序
  • 單線程執行結果不能被改變,使單線程不會被重排序干擾到,能夠按照預期結果輸出

volatile的語義,由之前JMM模型我們知道當我們要讀取一個變量時,首先是從主內存中讀取到本地局部緩存,修改變量值後,是由JMM擇機寫回主內存,並不是立即寫回,而volatile變量則是每次讀取都是從主內存中讀取,寫入時立馬寫回主內存,可以理解爲對一個volatile的讀能夠讀取到任意線程對該變量的最後寫入,但是需要注意的是對單個volatile變量的讀寫具有原子性,但是volatile++並不是原子性的操作,實際上是分成了先 讀取volatile變量在執行+1在寫回,這個過程並不是原子性的操作。
的語義,在鎖對應的代碼塊中,共享變量不是從本地局部變量讀取,而是直接從主內存中讀取,當鎖釋放的時候,共享變量直接寫回主內存,從這裏可以看出,鎖的語義與volatile的語義基本上相同。
synchronized語義,java中一般可以用synchronized來實現鎖的效果,具體形式爲:

  • 普通的同步方法,鎖是當前對象
  • 靜態同步方法,所示當前Class對象
  • 同步代碼塊,所示synchronized括號裏面的對象

JVM中實現synchronized主要是通過進入和退出Monitor對象,基於monitorentermonitorexit指令來實現,在java對象頭中MarkWord會有當前對象鎖的相關信息。
java中鎖一般有4中狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態

JVM 對象頭Mark Word
鎖升級過程:
         當程A訪問執行代碼並請求獲取對象鎖時,首先會比較當前線程ID與對象頭中偏向鎖的線程ID是否一致,如果一致,則表明當前線程A已經獲取到了鎖,繼續操作。如果不一致,則判斷對象頭中線程ID對應線程B是否存活,如果對應線程B不在存活,設置對象頭爲無鎖狀態,當前線程A獲取偏向鎖,並通過CAS設置對象頭偏向鎖線程ID爲當前線程A ID,如果線程B存活,判斷線程B是否需要繼續使用對象鎖,如果不需要,設置對象頭爲無鎖狀態,當前線程通過CAS設置對象頭偏向鎖線程ID,如果CAS設置失敗,表示有其他線程獲取鎖,升級爲偏向鎖。如果線程B不在需要當前鎖,則會將對象頭鎖設置爲無鎖狀態,線程A在執行獲取偏向鎖操作。
需要注意的是,偏向鎖不會主動釋放鎖,每次都是等另外線程來獲取鎖,纔會檢察之前的線程的偏向鎖是否需要釋放
當線程A請求獲取偏向鎖時,線程B已經獲取了偏向鎖,且線程B存活並在使用鎖,這時候jvm暫停線程B,將對象頭鎖設置爲偏向鎖,同時在線程A,B棧幀中創建存儲鎖記錄的空間,並將對象頭中Mark Word複製到A,B鎖記錄中,並將對象頭中輕量級鎖指向線程B棧中鎖記錄指針,線程A自旋嘗試使用CAS將對象頭中的Mark Word中鎖記錄指針指向線程A的棧中鎖記錄指針,此時線程B已經獲取,失敗,線程A嘗試自旋來獲取鎖
但是不能一直自旋,自旋是在消耗CPU的,當自旋到一定程度,仍未獲取到鎖的時候,這時候就會升級爲重量級鎖,這時候線程B已經獲取了輕量級鎖,jvm會將對象頭中鎖標誌改爲重量級鎖,同時會阻塞當前在請求鎖但是沒有獲取到鎖的線程。這個時候嘗試獲取鎖的線程都會被阻塞,當持有鎖的線程釋放鎖之後會喚醒這些線程,開啓新的鎖的競爭。
正如開始提到的,線程上下文切換會消耗資源影響速度,重量級鎖鎖被阻塞和獲取鎖之後實際上發生了線程上下文切換,但是如果輕量級鎖一直自旋,CPU會一直自旋浪費。

悲觀鎖,樂觀鎖,公平鎖非公平鎖,獨佔鎖,共享鎖,可重入鎖,自旋鎖

在同步或者鎖的過程中,基於阻塞方式會產生線程上線文切換的開銷,隨着現代計算機的發展和硬件的進步,在硬件和系統層面增加了CAS(Compare And Swap)支持,解決讀-改-寫等原子性問題,提供原子性操作。
CAS操作比較著名的是ABA問題:線程1首先獲取變量X的值(A),這時候線程2通過CAS修改X的值爲B,然後又使用CAS修改X的值爲A,這時候線程1使用CAS修改X的值(A)爲M,這時候的A與剛開始獲取的值A不是同一個概念了。JDK中AtomicStampedReference給沒給變量的狀態值都增加了一個時間戳,這樣每次操作除了值還會比較時間戳變量是否一致

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