JAVA線程安全與鎖優化

1.線程

線程是CPU調度的基本單位,是比進程更輕量級的調度執行單位。線程可以將一個進程的資源分配與執行調度分開,各個線程即可以共享進程資源,又可以獨立調度執行。

1.1.線程的實現方式

線程主要有三種實現方式。

1.1.1.使用內核線程實現

        內核線程(Kernel Thread,KLT)是直接由操作系統內核支持的線程,這種線程由內核來完成線程切換,內核通過操作調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上,支持多線程的內核叫做多線程內核(Muti-Threads Kernel)。
        輕量級進程(Light Weight Process,LWP)是內核線程的一種高級接口,程序一般也是通過LWP去使用內核進程,每個LWP都有一個內核線程支持,因此只有支持內核喜愛暱稱,纔能有輕量級進程。
        LWP基於內核線程實現,各種線程操作都需要進行系統調用,而系統調用需要在用戶態(User Mode)和內核態(Kernel Mode)之間切換,調用的代價較高。其次每個LWP都依賴一個內核線程,因此需要消耗一定的內核資源。
LWP與內核線程是1:1關係。
在這裏插入圖片描述

1.1.2.使用用戶線程實現

用戶線程(User Thread)是指完全建立在用戶控件的線程庫上,系統內核不能感知到線程的存在。用戶線程的建立、同步、銷燬和調度全完在用戶態中完成,不需要內核的幫助,也不需要切換到內核態,因此線程的操作快速且消耗低。但是線程的創建、切換和調度及多處理器中線程的映射都需要用戶程序自己處理,程序將變的異常複雜。
進程與用戶線程是1:N關係。
在這裏插入圖片描述

1.1.3.使用用戶線程加輕量級進程混合實現

混合實現是既存在用戶線程(UT),也存在輕量級進程(LWP),用戶線程還是建立在用戶空間,線程的創建、切換、解析等都是在用戶空間,而輕量級進程則作爲用戶線程和內核線程之間的交互橋樑,線程的調度及多處理器映射都通過LWP使用內核實現,這樣既能在用戶態實現線程的大量併發,也能利用內核態的調度功能。
用戶線程與LWP是M:N關係。
在這裏插入圖片描述

1.2.線程的調度方式

線程的調度方式有兩種:

  • 協同式
    協同式調度中,線程的執行時間由線程本身來控制,線程把執行完了之後,要主動通知系統切換到另外的線程上去。優點是實現簡單,缺點是線程執行時間不可控,如果一個線程編寫有問題,可能導致整個系統的崩潰。
  • 搶佔式
    搶佔式調用中,每個線程的執行時間由系統來分配,線程的切換由系統來決定,線程的執行時間可控,不會出現一個線程導致整個進程阻塞、崩潰的問題。

1.3.java中線程的實現方式

java中線程模型的映射基於操作系統原生線程模型來實現。因此不同系統上線程模型的映射是不同的。Windows和Linux都是使用一對一的線程模型,因此在Windows和Linux上Java線程模型的映射就是一個java線程映射到一個LWP上。
java中線程調度使用搶佔式。
線程的狀態:

  • 新建
    新建的尚未啓動的線程處於這種狀態。
  • 運行
    正在執行的線程(Running),或者等待着CPU分配執行時間(Ready)的線程。
  • 等待
    處於此狀態的線程不會被CPU分配執行時間,需要等待其他線程顯式的喚醒。
    無參的Object.wait()和Thread.join()和LockSupport.park()方法會讓線程進入此狀態。
  • 限時等待
    處於此狀態的線程不會被CPU分配執行時間,會在等待一定時間後被系統自動喚醒。
    Thread.sleep()和有參的Object.wait()和Thread.join()方法會進入此狀態。
  • 阻塞
    等待獲取排它鎖的線程會處於此狀態。線程在進入同步區域的時候進入此狀態。
  • 結束
    已終止線程的線程狀態,線程已經結束執行。
    狀態轉換圖如下:在這裏插入圖片描述

2.線程安全

如果一個對象可以被多個線程同時使用,並且都能可以獲取到正確的結果,那麼它就是線程安全的。

2.1.線程安全的幾種類型

  • 不可變類型
    被final修飾的變量,經初始化後完全不可變的情況
  • 相對線程安全類型
    指在對單個對象進行操作時是線程安全的,一般不需要進行額外的保障操作。如Vector、HashTable等操作方法都是被synchronized修飾的對象及併發包下的一些集合、對象都是相對線程安全的
  • 非線程安全類型
    指對象本身不是線程安全的,但可以在編寫代碼時候使用同步手段來保證對象在併發環境中安全的使用。如ArrayList、HashMap等。

2.2.線程安全的實現方式

2.2.1互斥同步

互斥同步是指在多個線程併發訪問共享數據時,保證共享數據在同一時刻只能被一條線程使用,其他線程排隊等待獲取資源。而synchronized、ReentantLock等都是實現互斥同步。

2.2.2.非阻塞同步

互斥同步是阻塞同步,線程阻塞和喚醒都會帶來性能問題,它也是一種悲觀的併發策略。
非阻塞同步是基於衝突檢測的樂觀併發策略,就是先進行操作,如果沒有其他線程爭搶共享數據就操作成功,如果有爭搶產生了衝突,就進行補償措施(一般就是重試,直到成功),這種操作的許多實現都不需要把線程掛起,因此這種操作稱爲非阻塞同步。
非阻塞同步要操作和衝突檢測保證原子性,而這需要硬件的處理器指令來保證,常用指令有:

  • 測試並設置(Test-and-Set)
  • 獲取並增加(Fetch-and-Increment)
  • 交換(Swap)
  • 比較並交換(Compare-and-Swap,CAS)
  • 加載鏈接/條件儲存(Load-Linked/Store-Conditional,LL/SC)
    jdk1.5之後java中可以使用CAS操作,sun.misc.Unsafe類裏面的compareAndSwapInt()、compareAndSwapLong()等方法提供包裝,而ReentantLock類中設置狀態值的改變調用AQS中的compareAndSetState()方法就是基於compareAndSwapInt

2.2.3.無同步方案

無同步方案指無需使用同步措施就能保證正確性。這分爲兩種情況:

  • 可重入代碼
    可重入代碼的特徵是不依賴存儲在堆上的數據和公用的系統資源、用到的變量都有參數傳入,不調用非可重入的方法等。
  • 線程本地存儲
    即把數據的可見範圍限制在同一個線程之內,這樣就能避免線程之間的數據爭搶問題。
    如果變量要被多個線程訪問,就用volatile修飾。如果變量要被一個線程共享,可以設置爲本地線程變量(ThreadLocal)。

3.鎖優化

鎖優化技術是爲了在線程之間更高效的共享數據,以解決競爭問題,從而提高程序的執行效率。

3.1.自旋鎖與自適應自旋

        互斥同步會出現阻塞的情況,線程的掛機和恢復都需要轉入內核態中完成,這些操作會嚴重影響系統的併發能力。大多數情況下,共享數據的鎖定狀態都只會持續很短的時間。爲了這個時間去掛起和恢復並不划算。如果物理機有多個處理器,可以讓兩個以上的線程並行執行,我們就可以讓後面請求鎖的線程暫停一下,但是不放棄處理器的執行時間,看看鎖是否會很快釋放。爲了讓線程暫停一下,我們要讓線程執行一個忙循環(自旋),這就是自旋鎖。自旋期間不會放棄處理器執行時間,因此如果鎖被佔用的時間很短,自旋的效果會很好,如果鎖的時間長,那麼只會浪費處理器的資源,造成性能浪費,因此自旋也會有對應的時間限度,如果超過這個時間還是會掛起線程。
        jdk1.6中默認開啓自旋鎖。默認次數是10次。
        自適應自旋指自旋的時間不固定,由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

  • 如果在同一個鎖對象上自旋等待剛剛成功獲取過鎖,並且持有鎖的線程正在運行中,虛擬機會認爲這次自旋也很有可能成功,就會允許它自旋等待持續更長的時間。
  • 如果對於某個所,自旋很少成功獲取過鎖,那在以後獲取這個鎖時候可能會省略到自旋的過程,以避免浪費處理器資源。

3.2.鎖消除

鎖消除是指虛擬機即時編譯器在運行時,對 一些代碼要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判斷依據來自於逃逸分析的數據支持,如果判斷一端代碼中,在堆上的所有數據都不會逃逸出去被其他線程訪問到,就可以把它們當做棧上數據對待,被認爲是線程私有的,就不需要加鎖。

3.3.鎖粗化

如果一系列的連續操作都是對同一個對象反覆的加鎖和解鎖,甚至加鎖是在循環中,這樣即使沒有線程競爭,反覆的互斥同步也會造成不必要的性能損耗。如StringBuffer中的append(),如果連續出現append(),虛擬機檢測到連續的多次對同一個對象加鎖,就會將加鎖範圍擴大(粗化)到整個操作序列的外部,這樣只需要加一次鎖就可以了,這個操作就是鎖粗化。

3.4.輕量級鎖

輕量級鎖意義是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。輕量級鎖是依賴對象頭中的“Mark Word”來實現的,“Mark Word”中又2位作爲鎖標誌位,其狀態如下

存儲內容 標誌位 狀態
對象哈希碼、對象分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 膨脹(重量級鎖定)
空,不需要標記信息 11 GC標記
偏向線程ID、偏向時間戳、對象分代年齡 01 可偏向

加鎖過程:

  1. 進入同步塊時候,如果同步對象沒有被鎖定(標誌位爲“01”狀態),虛擬機會在當前線程棧幀中創建鎖記錄(Lock Record)的空間,用於存儲 Mark Word的拷貝。
  2. 拷貝對象頭中的Mark Word複製到鎖記錄中。
  3. 拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針。如果更新成功,則執行步驟(4),否則執行步驟(5)
  4. 如果更新成功,那麼這個線程就擁有了該對象的鎖,並且將對象的Mark Word鎖標誌位改爲“00”,即表示此對象進入輕量級鎖定狀態。
  5. 如果更新失敗,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明鎖對象被其他線程搶佔,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

解鎖過程 就是通過CAS操作嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word,如果替換成功,那麼整個同步過程就完成了,如果替換失敗,說明有其他線程獲取過鎖,就要在釋放鎖的同時,喚醒其他線程。

3.5.偏向鎖

偏向鎖的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。
偏向鎖會偏向於第一個獲取它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要在進行同步。偏向鎖會提高帶有同步但無競爭的程序的性能。程序中如果大多數鎖都總是被多個不同線程訪問,那偏向模式就是多餘的。
加鎖過程:

  1. 當線程第一次獲取鎖對象時候,虛擬機會將Mark Word中標誌位設爲“01”,即偏向模式。同時使用CAS操作把獲取到的這個線程的ID記錄在對象的Mark Word中
  2. 如果操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛擬機都可以不再進行任何同步操作。
  3. 當有另外線程去獲取鎖時,偏向模式會結束。根據鎖對象目前是否處於被鎖定狀態,撤銷偏向後恢復到未鎖定(標誌位爲“01”)或輕量級鎖定(標誌位爲“00”)的狀態,後續的同步操作就如輕量級鎖一樣執行。

3.6. 偏向鎖、輕量級鎖、重量級鎖關係

在這裏插入圖片描述

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