若是想要透徹的理解java鎖的前因後果,須要先了解如下基礎知識。java
按照其性質分類算法
公平鎖是指多個線程按照申請鎖的順序來獲取鎖。非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會形成優先級反轉或者飢餓現象。對於Java ReentrantLock而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。對於Synchronized而言,也是一種非公平鎖。因爲其並不像ReentrantLock是經過AQS的來實現線程調度,因此並無任何辦法使其變成公平鎖。編程
樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。悲觀鎖認爲對於同一個數據的併發操做,必定是會發生修改的,哪怕沒有修改,也會認爲修改。所以對於同一個數據的併發操做,悲觀鎖採起加鎖的形式。悲觀的認爲,不加鎖的併發操做必定會出問題。樂觀鎖則認爲對於同一個數據的併發操做,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷從新的方式更新數據。樂觀的認爲,不加鎖的併發操做是沒有事情的。從上面的描述咱們能夠看出,悲觀鎖適合寫操做很是多的場景,樂觀鎖適合讀操做很是多的場景,不加鎖會帶來大量的性能提高。悲觀鎖在Java中的使用,就是利用各類鎖。樂觀鎖在Java中的使用,是無鎖編程,經常採用的是CAS算法,典型的例子就是原子類,經過CAS自旋實現原子操做的更新。安全
獨享鎖是指該鎖一次只能被一個線程所持有。共享鎖是指該鎖可被多個線程所持有。對於Java ReentrantLock而言,其是獨享鎖。可是對於Lock的另外一個實現類ReentrantReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是很是高效的,讀寫,寫讀 ,寫寫的過程是互斥的。獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。對於Synchronized而言,固然是獨享鎖。數據結構
上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。互斥鎖在Java中的具體實現就是ReentrantLock,讀寫鎖在Java中的具體實現就是ReentrantReadWriteLock多線程
可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。對於Java ReentrantLock而言, 他的名字就能夠看出是一個可重入鎖,其名字是Reentrant Lock從新進入鎖。對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可必定程度避免死鎖。併發
java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統介入,須要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。jvm
若是線程狀態切換是一個高頻操做時,這將會消耗不少CPU處理時間;
若是對於那些須要同步的簡單的代碼塊,獲取鎖掛起操做消耗的時間比用戶代碼執行的時間還要長,這種同步策略顯然很是糟糕的。
synchronized會致使爭用不到鎖的線程進入阻塞狀態,因此說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖,爲了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啓用了自旋鎖,他們都屬於樂觀鎖。ide
明確java線程切換的代價,是理解java中各類鎖的優缺點的基礎之一。函數
CAS(Compare and swap)比較和替換是設計併發算法時用到的一種技術。簡單來講,比較和替換是使用一個指望值和一個變量的當前值進行比較,若是當前變量的值與咱們指望的值相等,就使用一個新值替換當前變量的值。
Java5以來,你可使用java.util.concurrent.atomic包中的一些原子類來使用CPU中的這些功能:
private AtomicBoolean locked = new AtomicBoolean(false); public boolean lock() { return locked.compareAndSet(false, true); }
locked變量再也不是boolean類型而是AtomicBoolean。這個類中有一個compareAndSet()方法,它使用一個指望值和AtomicBoolean實例的值比較,和二者相等,則使用一個新值替換原來的值。在這個例子中,它比較locked的值和false,若是locked的值爲false,則把修改成true。
若是值被替換了,compareAndSet()返回true,不然,返回false。
CAS的ABA問題
這個例子你可能沒有看懂,維基百科上給了一個活生生的例子——
你拿着一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女, 而後她很暖昧地挑逗着你,並趁你不注意的時候,把用一個如出一轍的 手提箱和你那裝滿錢的箱子調了個包,而後就離開了,你看到你的手提 箱還在那,因而就提着手提箱去趕飛機去了。
前面提到了java的4種鎖,他們分別是重量級鎖、自旋鎖、輕量級鎖和偏向鎖,
不一樣的鎖有不一樣特色,每種鎖只有在其特定的場景下,纔會有出色的表現,java中沒有哪一種鎖可以在全部狀況下都能有出色的效率,引入這麼多鎖的緣由就是爲了應對不一樣的狀況;
前面講到了重量級鎖是悲觀鎖的一種,自旋鎖、輕量級鎖與偏向鎖屬於樂觀鎖,因此如今你就可以大體理解了他們的適用範圍,可是具體如何使用這幾種鎖呢,就要看後面的具體分析他們的特性;
Java SE1.6裏synchronized一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。
自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
可是線程自旋是須要消耗cpu的,說白了就是讓cpu在作無用功,若是一直獲取不到鎖,那線程也不能一直佔用cpu自旋作無用功,因此須要設定一個自旋等待的最大時間。
若是持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會致使其它爭用鎖的線程在最大等待時間內仍是獲取不到鎖,這時爭用線程會中止自旋進入阻塞狀態。
自旋鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來講性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起再喚醒的操做的消耗,這些操做會致使線程發生兩次上下文切換!
可是若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用cpu作無用功,佔着XX不XX,同時有大量線程在競爭一個鎖,會致使獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操做的消耗,其它須要cpu的線程又不能獲取到cpu,形成cpu的浪費。因此這種狀況下咱們要關閉自旋鎖;
JDK1.6中-XX:+UseSpinning開啓;
-XX:PreBlockSpin=10 爲自旋次數;
JDK1.7後,去掉此參數,由jvm控制;
Java偏向鎖(Biased Locking)是Java6引入的一項多線程優化。
偏向鎖,顧名思義,它會偏向於第一個訪問鎖的線程,
大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到。偏向鎖的目的是在某個線程得到鎖以後,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程獲得了偏護。另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的狀況也作了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種狀況,由於線程以前除了互斥以外也可能發生同步關係,被同步的兩個線程(一前一後)對共享對象鎖的競爭極可能是沒有衝突的。對這種狀況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地生成一個時間戳代價仍是蠻大的,所以這裏應當理解爲一種相似時間戳的identifier)
若是在運行過程當中,遇到了其餘線程搶佔鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復到標準的輕量級鎖。
它經過消除資源無競爭狀況下的同步,進一步提升了程序的運行性能。
偏向鎖的撤銷在上述第四步驟中有提到。偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。
始終只有一個線程在執行同步塊,在它沒有執行完釋放鎖以前,沒有其它線程去執行同步塊,在鎖無競爭的狀況下使用,一旦有了競爭就升級爲輕量級鎖,升級爲輕量級鎖的時候須要撤銷偏向鎖,撤銷偏向鎖的時候會致使stop the word操做;
在有鎖的競爭時,偏向鎖會多作不少額外操做,尤爲是撤銷偏向所的時候會致使進入安全點,安全點會致使stw,致使性能降低,這種狀況下應當禁用;
開啓偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
關閉偏向鎖:-XX:-UseBiasedLocking
輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖;
線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其餘線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。
輕量級解鎖時,會使用原子的CAS操做來將Displaced Mark Word替換回到對象頭,若是成功,則表示同步過程已完成。若是失敗,表示有其餘線程嘗試過獲取該鎖,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。則要在釋放鎖的同時喚醒被掛起的線程。
偏向鎖/輕量級鎖/重量級鎖
這三種鎖是指鎖的狀態,而且是針對Synchronized。在Java 5經過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是經過對象監視器在對象頭中的字段來代表的。
第一步,檢查MarkWord裏面是否是放的本身的ThreadId ,若是是,表示當前線程是處於 「偏向鎖」.跳太輕量級鎖直接執行同步體。
第二步,若是MarkWord不是本身的ThreadId,鎖升級,這時候,用CAS來執行切換,新的線程根據MarkWord裏面現有的ThreadId,通知以前線程暫停,以前線程將Markword的內容置爲空。
第三步,兩個線程都把對象的HashCode複製到本身新建的用於存儲鎖的記錄空間,接着開始經過CAS操做,把共享對象的MarKword的內容修改成本身新建的記錄空間的地址的方式競爭MarkWord.
第四步,第三步中成功執行CAS的得到資源,失敗的則進入自旋.
第五步,自旋的線程在自旋過程當中,成功得到資源(即以前獲的資源的線程執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖的狀態,若是自旋失敗 第六步,進入重量級鎖的狀態,這個時候,自旋的線程進行阻塞,等待以前線程執行完成並喚醒本身.
若是線程爭用激烈,那麼應該禁用偏向鎖。
以上介紹的鎖不是咱們代碼中可以控制的,可是借鑑上面的思想,咱們能夠優化咱們本身線程的加鎖操做;
不須要同步執行的代碼,能不放在同步快裏面執行就不要放在同步快內,可讓鎖儘快釋放;
它的思想是將物理上的一個鎖,拆成邏輯上的多個鎖,增長並行度,從而下降鎖競爭。它的思想也是用空間來換時間;
java中不少數據結構都是採用這種方法提升併發操做的效率:
ConcurrentHashMap的鎖分段技術
LinkedBlockingQueue也體現了這樣的思想,在隊列頭入隊,在隊列尾出隊,入隊和出隊使用不一樣的鎖,相對於LinkedBlockingArray只有一個鎖效率要高;
鎖的粗化則是要增大鎖的粒度;
在如下場景下須要粗化鎖的粒度:
假若有一個循環,循環內的操做須要加鎖,咱們應該把鎖放到循環外面,不然每次進出循環,都進出一次臨界區,效率是很是差的;
ReentrantReadWriteLock 是一個讀寫鎖,讀操做加讀鎖,能夠併發讀,寫操做使用寫鎖,只能單線程寫;
CopyOnWriteArrayList 、CopyOnWriteArraySet
咱們能夠對CopyOnWrite容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素,而是操做容器的副本。因此CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不一樣的容器。
若是須要同步的操做執行速度很是快,而且線程競爭並不激烈,這時候使用cas效率會更高,由於加鎖會致使線程的上下文切換,若是上下文切換的耗時比同步操做自己更耗時,且線程對資源的競爭不激烈,使用volatiled+cas操做會是很是高效的選擇;