本篇是 《深刻理解Java虛擬機》的最後一章, 在此涉及到了線程安全, 但並非如何從代碼層次來實現線程安全, 而是虛擬機自己對線程安全作出了哪些努力, 在安全與性能之間又採起了哪些優化措施.html
那麼一步步來梳理這些概念.java
參考linux
一個進程因爲其運行空間的不一樣, 從而有內核線程和用戶進程的區分, 內核線程運行在內核空間, 之因此稱之爲線程是由於它沒有虛擬地址空間, 只能訪問內核的代碼和數據.編程
而用戶進程則運行在用戶空間, 不能直接訪問內核的數據可是能夠經過中斷, 系統調用等方式從用戶態陷入內核態, 可是內核態只是進程的一種狀態, 與內核線程有本質區別.api
內核線程就是內核的分身, 一個分身能夠處理一件特定事情.內核線程的使用是廉價的, 惟一使用的資源就是內核棧和上下文切換時保存寄存器的空間.支持多線程的內核叫作多線程內核(Multi-Threads kernel ).瀏覽器
內核線程只運行在內核態,不受用戶態上下文的拖累.安全
處理器競爭:能夠在全系統範圍內競爭處理器資源;服務器
使用資源:惟一使用的資源是內核棧和上下文切換時保持寄存器的空間
調度: 調度的開銷可能和進程自身差很少昂貴
資源的同步和數據共享比整個進程的數據同步和共享要低一些.
內核線程沒有本身的地址空間,因此它們的」current->mm」都是空的, 且內核線程只能在內核空間操做,不能與用戶空間交互.
至於內核空間, 用戶空間, 內核態, 用戶態, 後文再來描述.
輕量級進程(LWP)是創建在內核之上並由內核支持的用戶線程,它是內核線程的高度抽象,每個輕量級進程都與一個特定的內核線程關聯.內核線程只能由內核管理並像普通進程同樣被調度.
而輕量級進程就是咱們一般意義上所說的線程.
與普通進程區別:LWP只有一個最小的執行上下文和調度程序所需的統計信息.
處理器競爭:因與特定內核線程關聯,所以能夠在全系統範圍內競爭處理器資源
使用資源:與父進程共享進程地址空間
調度:像普通進程同樣調度
每個進程有一個或多個LWPs,每一個LWP由一個內核線程支持.在這種實現的操做系統中,LWP就是用戶線程.
因爲每一個LWP都與一個特定的內核線程關聯,所以每一個LWP都是一個獨立的線程調度單元.即便有一個LWP在系統調用中阻塞,也不會影響整個進程的執行.
輕量級進程具備侷限性.
首先,大多數LWP的操做,如創建、析構以及同步,都須要進行系統調用.系統調用的代價相對較高:須要在user mode和kernel mode中切換.
其次,每一個LWP都須要有一個內核線程支持,所以LWP要消耗內核資源(內核線程的棧空間).所以一個系統不能支持大量的LWP.
注:
1.LWP的術語是借自於SVR4/MP和Solaris 2.x.
2.有些系統將LWP稱爲虛擬處理器.
3.將之稱爲輕量級進程的緣由多是:在內核線程的支持下,LWP是獨立的調度單元,就像普通的進程同樣.因此LWP的最大特色仍是每一個LWP都有一個內核線程支持.
狹義上的 用戶線程是徹底創建在用戶空間的線程庫,用戶線程的建立、調度、同步和銷燬全又庫函數在用戶空間完成, 系統不可以感知到線程存在的實現.所以這種線程是極其低消耗和高效的.能夠支持更大規模的線程數量.
但其優勢在於對於系統而言是透明的, 缺點也在於此, 對於操做系統而言, 只是將處理器的資源分配給進程, 進程是處理器資源的最小調度單位, 當在進程中, 一個用戶線程若是阻塞在系統調用中,則整個進程都將會阻塞.而一樣的, 由於核心信號(不管是同步的仍是異步的)都是以進程爲單位的,沒法定位到用戶線程線程,因此這種實現方式不能用於多處理器系統.
所以, 在現實中,純用戶級線程的實現,除算法研究目的之外,幾乎已經消失了.
用戶線程庫仍是徹底創建在用戶空間中,所以用戶線程的操做仍是很廉價,所以能夠創建任意多須要的用戶線程.
操做系統提供了LWP做爲用戶線程和內核線程之間的橋樑.LWP仍是和前面提到的同樣,具備內核線程支持,是內核的調度單元,而且用戶線程的系統調用要經過LWP,所以進程中某個用戶線程的阻塞不會影響整個進程的執行.
用戶線程庫將創建的用戶線程關聯到LWP上,LWP與用戶線程的數量不必定一致.當內核調度到某個LWP上時,此時與該LWP關聯的用戶線程就被執行.
對於Sun JDK來講, 它的Windows版與Linux版都是使用一對一的線程模型實現的, 一條Java線程就映射到一條輕量級進程之中, 由於Windows和Linux系統提供的線程模型就是一對一的.
在Solaris平臺中, 因爲操做系統的線程特性能夠同時支持一對一(經過Bound Threads或Alternate Libthread實現)及多對多(經過LWP/Thread Based Synchronization實現)的線程模型,所以在Solaris版的JDK中也對應提供了兩個平臺專有的虛擬機參數:-XX:+UseLWPSynchronization(默認值)和-XX:+UseBoundThreads來明確指定虛擬機使用哪一種線程模型.
但在這裏我有點實在不理解, 對於Linux系統而言, 輕量級進程也即一般意義上的線程數量是有限的, 也就意味着一臺Linux服務器所能支持的最大線程數不過是 幾百上千, 而就我所理解的, 在Java中, 對應每一個Controller請求都會建立相應的線程, 不止如此, 在代碼運行中, 程序員也可能手動建立線程執行任務, 如此多的線程到底是怎樣被支撐起來的? 而相應的不說百萬, 幾萬的併發量又是如何解決的?
即便是所謂的在多個線程之間進行切換, 但單臺計算機自己可以建立的線程數是有限的, 特別是在Linux系統下, 一個線程就是一個輕量級進程, 只不過是剛好與其餘 進程 共享 資源而已.
我所經手的系統, 還從未遇到過大併發的狀況, 因此概念都只在猜想中.
但就目前瞭解到的而言:
Tomcat 默認配置的最大請求數是 150, 也就是說同時支持 150 個併發. 雖然能夠自由調整, 其實也意味着同一臺服務器所支持的併發數量必然不能太多, 更高的應該是要用分佈式來解決這個問題.
建立的每個線程都是要消耗CPU資源的, 種種因素就決定了服務器的最大承受併發數量.
關於這一段能夠跳過, 由於這種種概念與以前所提到的線程關聯並非很緊密, 更多的是一種對其中概念的補充說明, 而內核態與用戶態之間的切換, 與線程之間的切換分屬於不一樣層面的東西.
參考
是誰來劃份內存空間的呢?在電腦開機以前, 內存就是一塊原始的物理內存.什麼也沒有.開機加電, 系統啓動後, 就對物理內存進行了劃分.固然, 這是系統的規定, 物理內存條上並無劃分好的地址和空間範圍.這些劃分都是操做系統在邏輯上的劃分.不一樣版本的操做系統劃分的結果都是不同的.
內核空間中存放的是內核代碼和數據, 而進程的用戶空間中存放的是用戶程序的代碼和數據.無論是內核空間仍是用戶空間, 它們都處於虛擬空間中.
劃分不一樣區域的目的, 爲了讓程序之間互不干擾, 即便電腦的瀏覽器崩潰, 也不會致使電腦藍屏, 處於用戶態的程序只能訪問用戶空間, 而處於內核態的程序能夠訪問用戶空間和內核空間.那麼用戶態和內核態有什麼區別呢?
當一個任務(進程)執行系統調用而陷入內核代碼中執行時, 咱們就稱進程處於內核運行態(或簡稱爲內核態).此時處理器處於特權級最高的(0級)內核代碼中執行.當進程處於內核態時, 執行的內核代碼會使用當前進程的內核棧.每一個進程都有本身的內核棧.當進程在執行用戶本身的代碼時, 則稱其處於用戶運行態(用戶態).即此時處理器在特權級最低的(3級)用戶代碼中運行.
而由用戶態切換到內核態的方式有三種:
a. 系統調用
這是用戶態進程主動要求切換到內核態的一種方式, 用戶態進程經過系統調用申請使 用操做系統提供的服務程序完成工做, 好比前例中fork()實際上就是執行了一個建立新進程的系統調用.而系統調用的機制其核心仍是使用了操做系統爲用戶 特別開放的一箇中斷來實現, 例如Linux的int 80h中斷.
系統調用其實是應用程序在用戶空間激起了一次軟中斷, 在軟中斷以前要按照規範, 將各個須要傳遞的參數填入到相應的寄存器中.軟中斷會激起內核的異常處理, 此時就會強制陷入內核態(此時cpu運行權限提高), 軟中斷的異常處理函數會根據應用軟件的請求來決定api調用是否合法, 若是合法選擇須要執行的函數, 執行完畢後軟中斷會填入返回值, 安全地下降cpu權限, 將控制權交還給用戶空間.因此內核提供的api調用, 你徹底能夠認爲就是一個軟件包, 只不過這些軟件包你不能控制, 只能請求內核幫你執行.
b. 異常
當CPU在執行運行在用戶態下的程序時, 發生了某些事先不可知的異常, 這時會觸發由當前運行進程切換處處理此異常的內核相關程序中, 也就轉到了內核態, 好比缺頁異常.
c. 外圍設備的中斷
當外圍設備完成用戶請求的操做後, 會向CPU發出相應的中斷信號, 這時CPU會 暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序, 若是先前執行的指令是用戶態下的程序, 那麼這個轉換的過程天然也就發生了由用戶態到 內核態的切換.好比硬盤讀寫操做完成, 系統會切換到硬盤讀寫的中斷處理程序中執行後續操做等.
參考
線程調度是指系統爲線程分配處理器使用權的過程, 主要調度方式分兩種, 分別是協同式線程調度和搶佔式線程調度.
協同式線程調度, 線程執行時間由線程自己來控制, 線程把本身的工做執行完以後, 要主動通知系統切換到另一個線程上.最大好處是實現簡單, 且切換操做對線程本身是可知的, 沒啥線程同步問題.壞處是線程執行時間不可控制, 若是一個線程有問題, 可能一直阻塞在那裏.
搶佔式調度, 每一個線程將由系統來分配執行時間, 線程的切換不禁線程自己來決定(Java中, Thread.yield()可讓出執行時間, 但沒法獲取執行時間).線程執行時間系統可控, 也不會有一個線程致使整個進程阻塞.
Java線程調度就是搶佔式調度.
若是有興趣能夠了解下: 進程切換 這個概念
Java線程狀態:
NEW 狀態是指線程剛建立,還沒有啓動,不會出如今Dump中.
RUNNABLE 狀態是線程正在正常運行中, 固然可能會有某種耗時計算/IO等待的操做/CPU時間片切換等, 這個狀態下發生的等待通常是其餘系統資源, 而不是鎖, Sleep等, 主要不一樣是runable裏面有2個狀態, 能夠理解爲就是JVM調用系統線程的狀態.
BLOCKED 受阻塞並等待監視器鎖.這個狀態下, 是在多個線程有同步操做的場景, 好比正在等待另外一個線程的synchronized 塊的執行釋放, 或者可重入的 synchronized塊裏別人調用wait() 方法, 也就是這裏是線程在等待進入臨界區
WAITING 無限期等待另外一個線程執行特定操做.這個狀態下是指線程擁有了某個鎖以後, 調用了他的wait方法, 等待其餘線程/鎖擁有者調用 notify / notifyAll 一遍該線程能夠繼續下一步操做, 這裏要區分 BLOCKED 和 WATING 的區別, 一個是在臨界點外面等待進入, 一個是在臨界點裏面wait等待別人notify, 線程調用了join方法 join了另外的線程的時候, 也會進入WAITING狀態, 等待被他join的線程執行結束
TIMED_WAITING 有時限的等待另外一個線程的特定操做.這個狀態就是有限的(時間限制)的WAITING, 通常出如今調用wait(long), join(long)等狀況下, 另一個線程sleep後, 也會進入TIMED_WAITING狀態
TERMINATED 這個狀態下表示 該線程的run方法已經執行完畢了, 基本上就等於死亡了(當時若是線程被持久持有, 可能不會被回收)
線程安全
多個線程訪問同一個對象時, 若是不用考慮這些線程在運行時環境下的調度和交替執行, 也不須要進行額外的同步, 或者在調用方進行任何其餘操做, 調用這個對象的行爲均可以得到正確的結果, 那麼這個對象就是線程安全的.
而就目前的感受,只要當須要考慮到線程安全的時候, 才須要提到鎖這個概念.
而討論線程安全的前提, 是在多個線程之間共享數據. 若是沒有數據共享, 或根本沒有多線程, 那麼討論線程安全也是沒有意義的.
那麼除了在Java內存模型中提到的Happens-before之外, 比較有總結性質的, Java中線程安全的描述以下:
不可變
在Java語言中(1.5內存模型修正之後), 不可變對象必定是線程安全的, 不管是對象的方法實現仍是對象的調用者,都不須要再採起任何線程安全的保障措施, 只要一個不可變對象被正確的構建出來, 那麼其外部狀態就永遠不會改變, 則必然是線程安全的.
而對於基本類型而言, 用final關鍵字進行描述的屬性, 必然是不可變的, 這點在Java的內存模型中就有所定義, Java虛擬實現也對其作了相應保障.
而若是是個對象呢?須要保證的就是對象的行爲不會對其狀態產生任何影響.這個概念是什麼意思呢?
不妨參考:
Java多線程編程之不可變對象模式
不可變對象指的是, 對象內部沒有提供任何可供修改對象數據的方法, 若是須要修改共享變量的任何數據, 都須要先構建整個共享對象, 而後對共享對象進行總體的替換, 經過這種方式來達到對共享對象數據一致性的保證.
在設計時,須要注意如下幾點:
不可變對象的屬性必須使用final修飾, 以防止屬性被意外修改, 而且final還能夠保證JVM在該對象構造完成時該屬性已經初始化成功(JVM在構造完對象時可能只是爲其分配了引用空間, 而各個屬性值可能還未初始化完成, (僅僅用private 修飾是不夠的, 由於private沒法保證在jvm初始化時的線程安全.)
屬性的設值必須在構造方法中統一構造完成, 其他的方法只是提供的查詢各個屬性相關的方法;
對於可變狀態的引用類型屬性, 如集合, 在獲取該類型的屬性時, 必須返回該屬性的一個深度複製結果, 以防止不可變對象的該屬性值被客戶端修改;
不可變對象的類必須使用final修飾, 以防止子類對其自己或其方法進行修改;
絕對線程安全
絕對線程安全就是符合本節開頭定義的線程安全, 可是這代價每每是很大的, 甚至是不切實際的, 即便在JavaApi中標明瞭爲線程安全的類, 在使用時一樣須要注意, 由於它也不是絕對意義上的線程安全.
對於java.util.Vector而言, 幾乎全部的方法都用 synchronized進行修飾, 但這依然不能保證線程安全.
至於代碼就不貼在這裏了, 若是想看的話, 在 深刻理解java虛擬機第二版, 第十三章就有. 究其根本緣由, 不過是由於檢測與更改, 調用, 這些方法雖然自己具備原子性,可是聯合調用 是不具備原子性的.
因此在javaApi中的線程安全,大都是相對安全的.
相對線程安全
相對線程安全就是一般意義上的線程安全, 它須要保證對這個對象的單獨操做是線程安全的, 咱們在調用時無需考慮線程安全問題, 可是在組合調用時, 依然須要進行保障, 在java中, Vector, HashTable, Collections.synchronizedCollection()等其餘都是這種相對安全的.
線程兼容
指的是對象自己不是安全的, 可是能夠經過在調用端正確的使用同步手段, 保證線程安全, 而平時所說的線程不安全, 即指這一類狀況. 這種操做咱們經常會用到, 即進行加鎖操做.
線程對立
指的是不管採起怎樣的措施在多線程狀況下都沒法同時調用的代碼.
參考
互斥同步
互斥同步是一種常見的併發正確性保證手段, 主要是保證在多線程併發狀況下, 在同一時刻, 只可以被一個線程(或一些, 使用信號量的狀況下)使用.
互斥是實現同步的主要方式, 而實現的互斥的主要方式有:
二元信號量
二元信號量(Binary Semaphore)是一種最簡單的鎖, 它有兩種狀態:佔用和非佔用.它適合只能被惟一一個線程獨佔訪問的資源.當二元信號量處於非佔用狀態時, 第一個試圖獲取該二元信號量鎖的線程會得到該鎖, 並將二元信號量鎖置爲佔用狀態, 以後其它試圖獲取該二元信號量的線程會進入等待狀態, 直到該鎖被釋放.
信號量
多元信號量容許多個線程訪問同一個資源, 多元信號量簡稱信號量(Semaphore), 對於容許多個線程併發訪問的資源, 這是一個很好的選擇.一個初始值爲N的信號量容許N個線程併發訪問.線程訪問資源時首先獲取信號量鎖, 進行以下操做:
訪問資源結束以後, 線程釋放信號量鎖, 進行以下操做:
互斥量
互斥量(Mutex)和二元信號量相似,資源僅容許一個線程訪問.與二元信號量不一樣的是,信號量在整個系統中能夠被任意線程獲取和釋放,也就是說,同一個信號量能夠由一個線程獲取而由另外一線程釋放.而互斥量則要求哪一個線程獲取了該互斥量鎖就由哪一個線程釋放,其它線程越俎代庖釋放互斥量是無效的.
臨界區
臨界區(Critical Section)是一種比互斥量更加嚴格的同步手段.互斥量和信號量在系統的任何進程都是可見的,也就是說一個進程建立了一個互斥量或信號量,另外一進程試圖獲取該鎖是合法的.而臨界區的做用範圍僅限於本進程,其它的進程沒法獲取該鎖.除此之處,臨界區與互斥量的性質相同.
而在java中, 最基本的互斥同步手段就是經過 synchronized, 這個關鍵字在通過編譯以後, 會生成 monitorenter 和 monitorexit 指令, 分別對應進入同步塊 和退出 同步塊, 這兩個指令都須要一個 reference類型的參數來指明須要解鎖, 加鎖的對象, 這一點則和對象頭有關, 稍後會提到.
至於synchronized選取的對象, 若是指定了對象, 無需多言, 而若是沒有指定, 那麼就根據鎖定的 方法到底是實例方法仍是類方法, 去取對應的實例對象 又或者是 Class對象.
synchronized採起的方式就是互斥量, 不過是可重入的, 當須要獲取鎖的時候, 會去檢測當前線程是否已經擁有了相應對象的鎖, 若是已經擁有, 則計數器+1, 噹噹前線程釋放鎖的時候, 計數器減一. 當計數器爲0表示當前對象沒有被任何線程佔用, 鎖處於空閒狀態. 若是沒有, 則須要將線程阻塞, 等到鎖被釋放的時候再對線程進行喚醒.
然而, 在Java中, 線程的實現是與操做系統相掛鉤的, 所以線程的阻塞, 喚醒都須要系統級別的支持, 須要將當前線程從用戶態轉移到核心態. 而若是線程阻塞時間很短, 又須要將線程喚醒, 這種切換狀態的耗時甚至可能已經超過了執行代碼自己的耗時, 是一種很是消耗資源, 時間的行爲.
所以在Java1.6之後, 以經對synchronized作出了至關程度的優化. 而鎖的另外一種代碼實現, ReentrantLock, 在1.6之後的版本上, 性能上的問題, 已經不是決定選擇這兩種鎖中哪種的根本緣由.
ReentrantLock能夠實現這樣幾種特別的需求:
等待可中斷, 也就是設定等待時間, 超過以後,線程再也不等待, 執行別的.
可實現公平鎖, 在構造器中加入相應參數便可指定是否爲公平鎖, 默認是非公平鎖, 公平鎖是指多個線程等待同一把鎖的時候, 根據申請的前後順序來依次獲取鎖.
鎖綁定多條件 當線程wait()以後, 能夠被其餘線程喚醒, 可是喚醒具備隨機性, 即在等待當前鎖中的任何一個線程, 而喚醒條件 就是 在等待當前鎖的線程,能夠經過不斷嵌套, 將喚醒的線程最終鎖定到某一個, 但這無疑是很不方便的, 所以想要指定更多喚醒條件時, 就須要經過 ReentrantLock newCondition();
至於newCondition()的使用:
參考
因此在非上述幾種狀況下, 仍是使用 synchronized 比較合適, 這也是jvm優化的重點關注.
非阻塞同步
線程阻塞喚醒帶來的主要問題就是性能問題, 這種同步屬於互斥同步, 互斥同步是一種悲觀同步, 即, 認爲只要不進行同步操做就必定會帶來安全問題, 不管是否真的須要同步, 有共享資源, 都會進行加鎖操做 以及 用戶態核心態轉換, 維護鎖計數器, 以及是否有線程須要被喚醒的檢測操做.
而隨着硬件指令集發展, 出現了另外一種選擇, 基於衝突檢測的樂觀併發策略, 通俗來講, 就是先操做, 若是成功了, 繼續, 若是不成功則加鎖.
而問題來了, 檢測操做是兩個動做, 線程不安全的核心問題也大都源於此, 那這種樂觀鎖又是如何保證的? 這就 硬件指令集的支持, 大多人可能已經有所耳聞, 即 CAS;
比較, 若是符合條件就更新, 若是不知足, 則返回原值.比較並交換 是一個原子性操做, 再也不是被拆分紅兩步執行, 這也是樂觀鎖的核心所在, 既然已經知道了核心本質. 那麼如何使用就再也不是一個問題.網上資料很多, 我就再也不費力.
而CAS操做由 sun.misc.Unsafe類裏面的 compareAndSwapInt() 和 compareAndSwapLong() 提供, 虛擬機對這些方法作了特殊處理, 編譯出來就是一條CAS指令. 而咱們要是用的則是:
java.util.concurrent.atomic包下面的各類類便可. 若是使用基礎類型的話, 考慮到線程安全, 使用這些類已經能夠保證線程安全了.
而至於ABA問題, 通常狀況下並不會產生實質性的影響, 若是想要避免仍是使用互斥同步來進行實現便可.
無同步方案
如同一開始所提到的, 當線程之間不存在共享變量, 不存在數據競爭, 天然不須要同步操做.
而這裏要提到的就是另外一個類 ThreadLocal, 顧名思義, 本地線程, 對於每個線程都會建立對應的副本, 具體用法並不想在這裏多作說明. 很容易就找到不少例子.
它的核心實現大概是將當前線程 與 一張哈希表相關聯, 便可.
那麼該用在什麼地方呢? 若是有一個變量, 咱們須要進行共享, 卻僅限於當前線程內進行共享操做, 就須要用到ThreadLocal, 對於通常的共享變量, 每每會致使線程安全問題, 又沒有辦法將實例控制在單一線程內產生或銷燬, ThreadLocal就提供了這樣一個功能. 就像session, 須要在一個會話中保存對應的數據, 卻又不但願被其餘線程感知, 共享.
在使用上, 須要注意內存泄露的問題, 特別是使用線程池的時候, 具體緣由與ThreadLocal的實現有關.
能夠參考: ThreadLocal可能引發的內存泄露
在細說鎖以前, 還須要瞭解一個概念: monitor, 這就牽扯到了 Synchronized在底層究竟如何實現, 咱們知道鎖用的是對象, 那麼究竟如何標識這個對象, 以標識對象已經被鎖定?
參考
Java虛擬機給每一個對象和class字節碼都設置了一個監聽器Monitor,用於檢測併發代碼的重入,同時在Object類中還提供了notify和wait方法來對線程進行控制.
在Java的設計中 ,每個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者monitor.
每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表.每個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用, 其結構以下:
Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL;
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程。
RcThis:表示blocked或waiting在該monitor record上的全部線程的個數。
Nest:用來實現重入鎖的計數。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
對照圖來講, 機制以下:
Monitor能夠類比爲一個特殊的房間,這個房間中有一些被保護的數據,Monitor保證每次只能有一個線程能進入這個房間進行訪問被保護的數據,進入房間即爲持有Monitor,退出房間即爲釋放Monitor。
當一個線程須要訪問受保護的數據(即須要獲取對象的Monitor)時,它會首先在entry-set入口隊列中排隊(這裏並非真正的按照排隊順序),若是沒有其餘線程正在持有對象的Monitor,那麼它會和entry-set隊列和wait-set隊列中的被喚醒的其餘線程進行競爭(即經過CPU調度),選出一個線程來獲取對象的Monitor,執行受保護的代碼段,執行完畢後釋放Monitor,若是已經有線程持有對象的Monitor,那麼須要等待其釋放Monitor後再進行競爭。
再說一下wait-set隊列。當一個線程擁有Monitor後,通過某些條件的判斷(好比用戶取錢發現帳戶沒錢),這個時候須要調用Object的wait方法,線程就釋放了Monitor,進入wait-set隊列,等待Object的notify方法(好比用戶向帳戶裏面存錢)。當該對象調用了notify方法或者notifyAll方法後,wait-set中的線程就會被喚醒,而後在wait-set隊列中被喚醒的線程和entry-set隊列中的線程一塊兒經過CPU調度來競爭對象的Monitor,最終只有一個線程能獲取對象的Monitor。
參考
總的來講, 種種鎖的分類, 實現, 目的是爲了儘量的細粒度化加鎖, 也就是在絕對須要用到鎖的地方加鎖. 至於這個'絕對須要' 自己的定義就成爲了種種鎖設計的動機所在. 鎖的相關東西, 偏向於底層, 依賴於操做系統, 種種優化也大可能是在jvm層面進行優化, 所以對其實現暫時並無過高興致.
從調度策略上來講, 有公平鎖 非公平鎖, 固然在Java中要使用非公平鎖就須要ReentrantLock, 公平鎖指的是根據申請鎖的前後順序分配鎖給對應線程. 而非公平鎖則指的是 將當鎖被釋放以後, 隨機執行一個正在等待這個鎖的線程.
而樂觀鎖, 悲觀鎖指的並不是是某一特定的鎖, 而是一種思想.
a. 悲觀鎖認爲對於同一個數據的併發操做,必定是會發生修改的,哪怕沒有修改,也會認爲存在競爭.所以對於同一個數據的併發操做,悲觀鎖採起加鎖的形式.悲觀的認爲,不加鎖的併發操做必定會出問題.
b. 樂觀鎖則認爲對於同一個數據的併發操做,是不會發生修改的.在更新數據的時候,會採用嘗試更新,不斷從新的方式更新數據.樂觀的認爲,不加鎖的併發操做是沒有事情的.
從上面的描述咱們能夠看出,悲觀鎖適合寫操做很是多的場景,樂觀鎖適合讀操做很是多的場景,不加鎖會帶來大量的性能提高.
悲觀鎖在Java中的使用,就是利用各類鎖.
樂觀鎖在Java中的使用,是無鎖編程,經常採用的是CAS算法,典型的例子就是原子類,經過CAS自旋實現原子操做的更新.
自旋鎖
這是JVM進行的一種優化, 對於須要同步的代碼而言, 即便兩個線程在爭用同一把鎖, 也不會使得另外一個線程馬上進入阻塞狀態, 不難想象, 當要執行的代碼自己很是少時, 阻塞狀態的切換, 喚醒等操做所須要消耗的時間, 性能影響都已經超過了執行代碼自己時, 直接切入阻塞狀態無疑是一件比較不友好的事情.
在這裏就採起了一種自旋操做, 在每一次自旋中都須要斷定是否鎖已經被釋放, 若是釋放, 獲取鎖, 若是沒有則繼續自旋. 經過這種方式, 就避免了頻繁的 不合時宜的 阻塞.
而自旋的次數, 用戶能夠經過 -XX:PreBlockSpin來進行修改, 默認是10次.
而自旋在1.6之後成爲了自適應的, 若是在前一次獲取鎖的時候很快速, 而且成功了, 那麼本次會多等一會, 若是不多得到成功, 那麼會跳過自旋操做, 直接進入阻塞狀態.
輕量級鎖
輕量級鎖是相對於使用操做系統的互斥量實現的"重量級鎖"而言的.而輕量級鎖並非用來代替重量級鎖的, 本意是爲了減小多線程進入互斥的概率,並非要替代互斥.
要理解輕量級鎖, 須要瞭解 Mark Word這個概念, 虛擬機的對象頭包含兩部分, 第一部分用於存儲自身的運行時信息, 如 哈希碼, GC分代年齡等, 這部分被稱爲 Mark Word, 它是實現輕量級鎖和偏向鎖的關鍵, Mark Word被設計成一個非固定的數據結構,以便在最小的空間內存儲儘可能多的信息.
在對象未被鎖定時, 存儲信息在32位虛擬機下,mark word32bit的信息, 其中25bit用來存儲對象的哈希碼,4bit存儲GC分代年齡信息,兩bit存儲鎖標誌位, 剩下1bit固定位0. 而在其餘的狀態下, 以下表所示:
存儲內容 | 標誌位 | 狀態 |
對象哈希碼, 對象分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖(monitor)的指針 | 10 | 膨脹(重量級鎖) |
空, 不須要記錄信息 | 11 | GC標記 |
偏向線程ID,偏向時間戳,對象分代年齡 | 01 | 可偏向 |
在代碼進入同步塊時, 若是對象沒有被鎖定, 虛擬機首先在當前線程的棧幀中創建一個名爲 Lock Record(鎖記錄)的空間, 用於存儲對象目前的 Mark Word的拷貝.
而後嘗試以CAS的方式將對象的 Mark Word更新爲指向Lock Record的指針, 若是更新成功, 則表示線程已經擁有了對象的鎖, 並將對象的 Mark Word的標誌位設置爲00, 標識當前是處於 輕量級鎖定狀態下.
若是更新失敗, 則檢查是否其 Mark Record是不是指向當前線程的棧幀, 若是是的話, 表示已經獲取到鎖, 那麼就能夠直接進入同步塊執行, 不然說明當前鎖已經被其餘線程搶佔了.
若是兩個線程爭用同一把鎖, 不論這種爭用是發生在 CAS更新失敗, 仍是在初始時鎖已經被佔用, 那麼輕量級鎖就再也不有效, 須要膨脹爲重量級鎖, 不只僅Mark Word的標誌位要變成"10", 而Mark Word中存儲的指針也要變成指向 重量級鎖(互斥量, monitor)的指針, 後面等待鎖的線程也要進入阻塞狀態.
而釋放的時候, 也是經過CAS操做來進行, 若是Mark Word 仍然指向線程的 Lock Record, 則將Mark Word 與 Lock Record中存儲的 Mark Word替換, 若是直接替換成功, 則表示釋放鎖, 若是替換不成功, 則說明其已經膨脹爲重量級鎖, 有其餘線程嘗試獲取當前鎖, 則 仍然是經過 Mark Word中存儲的 monitor指針, 找到 wait set, 根據策略喚醒相應的線程.
偏向鎖
假設虛擬機已經啓動了偏向鎖(-XX:+UseBiasedLocking, 在1.6中默認啓動), 那麼當鎖第一次被線程獲取到時, 虛擬機會將對象的 標識位 置爲 01, 同時將線程的 ID 記錄在對象的 Mark Word中, 若是CAS操做成功, 持有偏向鎖的線程之後在每次進入相關同步塊時, 虛擬機都無需進行任何同步操做(Locking, unlocking, mark word update等)
而一旦有另外一個線程嘗試獲取鎖時, 偏向模式即宣告結束, 若是對象目前未被鎖定, 則撤銷恢復至未鎖定狀態, 若是對象已經鎖定, 那麼升級成爲輕量級鎖, 其操做就再也不多說.
且無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖, 鎖只能升級, 不能降級.