synchronized是基于jvm底層實現(xiàn)的數(shù)據同步,lock是基于Java編寫,主要通過硬件依賴CPU指令實現(xiàn)數(shù)據同步。下面一一介紹

一、synchronized的實現(xiàn)方案

  1.synchronized能夠把任何一個非null對象當成鎖,實現(xiàn)由兩種方式:

  a.當synchronized作用于非靜態(tài)方法時,鎖住的是當前對象的事例,當synchronized作用于靜態(tài)方法時,鎖住的是class實例,又因為Class的相關數(shù)據存儲在永久帶,因此靜態(tài)方法鎖相當于類的一個全局鎖。

  b.當synchronized作用于一個對象實例時,鎖住的是對應的代碼塊。

  2.synchronized鎖又稱為對象監(jiān)視器(object)。

      3.當多個線程一起訪問某個對象監(jiān)視器的時候,對象監(jiān)視器會將這些請求存儲在不同的容器中。

  >Contention List:競爭隊列,所有請求鎖的線程首先被放在這個競爭隊列中

  >Entry List:Contention List中那些有資格成為候選資源的線程被移動到Entry List中

  >Wait Set:哪些調用wait方法被阻塞的線程被放置在這里

  >OnDeck:任意時刻,最多只有一個線程正在競爭鎖資源,該線程被成為OnDeck

  >Owner:當前已經獲取到所資源的線程被稱為Owner

  > !Owner:當前釋放鎖的線程

  下圖展示了他們之前的關系

      iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

  4.synchronized在jdk1.6之后提供了多種優(yōu)化方案:

  >自旋鎖

    jdk1.6之后默認開啟,可以使用參數(shù)-XX:+UseSpinning控制,自旋等待不能代替阻塞,且先不說對處理器數(shù)量的要求,自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,因此,如果鎖被占用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被占用的時候很長,那么自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。自旋次數(shù)的默認值是 10 次,用戶可以使用參數(shù) -XX:PreBlockSpin 來更改。

    自旋鎖的本質:執(zhí)行幾個空方法,稍微等一等,也許是一段時間的循環(huán),也許是幾行空的匯編指令。

  >鎖消除

    即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據競爭的鎖進行消除,依據來源于逃逸分析的數(shù)據支持,那么是什么是逃逸分析?對于虛擬機來說需要使用數(shù)據流分析來確定是否消除變量底層框架的同步代碼,因為有許多同步的代碼不是自己寫的。

例1.1

public static String concatString(String s1, String s2, String s3) {       return s1 + s2 + s3;   }

  由于 String 是一個不可變的類,對字符串的連接操作總是通過生成新的 String 對象來進行的,因此 Javac 編譯器會對 String 連接做自動優(yōu)化。在 JDK 1.5 之前,會轉化為 StringBuffer 對象的連續(xù) append() 操作,在 JDK 1.5 及以后的版本中,會轉化為 StringBuilder 對象的連續(xù) append() 操作,這里的stringBuilder.append是線程不同步的(假設是同步)。

  Javac 轉化后的字符串連接代碼為:

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

public static String concatString(String s1, String s2, String s3) {       StringBuffer sb = new StringBuffer();       sb.append(s1);       sb.append(s2);       sb.append(s3);       return sb.toString();   }

iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

  此時的鎖對象就是sb,虛擬機觀察變量 sb,很快就會發(fā)現(xiàn)它的動態(tài)作用域被限制在 concatString() 方法內部。也就是說,sb 的所有引用永遠不會 “逃逸” 到concatString() 方法之外,其他線程無法訪問到它,雖然這里有鎖,但是可以被安全地消除掉,在即時編譯之后,這段代碼就會忽略掉所有的同步而直接執(zhí)行了。

  >鎖粗化

  將同步塊的作用范圍限制得盡量小——只在共享數(shù)據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快拿到鎖。

  >輕量級鎖

  加鎖過程:在代碼進入同步塊的時候,如果此同步對象沒有被鎖定(鎖標志位為 “01” 狀態(tài))虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的 Mark Word 的拷貝,這時候線程堆棧與對象頭的狀態(tài)如圖 13-3 所示

   iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

  然后,虛擬機將使用 CAS 操作嘗試將對象的 Mark Word 更新為指向 Lock Record 的指針。如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象 Mark Word 的鎖標志位 (Mark Word 的最后 2bit)將轉變?yōu)?“00”,即表示此對象處于輕量級鎖定狀態(tài),這時線程堆棧與對象頭的狀態(tài)如圖13-4                iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

  如果上述更新操作失敗,則說明這個鎖對象被其他鎖占用,此時輕量級變?yōu)橹亓考夋i,標志位為“10”,后面等待的線程進入阻塞狀態(tài)。

  解鎖過程:也是由CAS進行操作的,如果對象的 Mark Word 仍然指向著線程的鎖記錄,那就用 CAS 操作把對象當前的 Mark Word 和線程中復制的 Displaced Mark Word 替換回來,如果替換成功,整個同步過程就完成了。如果替換失敗,說明有其他線程嘗試過獲取該鎖,那就要釋放鎖的同時,喚醒被掛起的線程。

  輕量級鎖能提升程序同步性能的依據是 “對于絕大部分的鎖,在整個同步周期內都是不存在競爭的”,這是一個經驗數(shù)據。如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發(fā)生了 CAS 操作,因此在有競爭的情況下,輕量級鎖會比傳統(tǒng)的重量級鎖更慢。

  >偏向鎖

  偏向鎖也是 JDK 1.6 中引入的一項鎖優(yōu)化,它的目的是消除數(shù)據在無競爭情況下的同步原語,進一步提高程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用 CAS 操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除掉,連 CAS 操作都不做了。

  實質就是設置一個變量,判斷這個變量是否是當前線程,是就避免再次加鎖解鎖操作,從而避免了多次的CAS操作。壞處是如果一個線程持有偏向鎖,另外一個線程想爭用偏向對象,擁有者想釋放這個偏向鎖,釋放會帶來額外的性能開銷,但是總體來說偏向鎖帶來的好處還是大于CAS的代價的。在具體問題具體分析的前提下,有時候使用參數(shù) -XX:-UseBiasedLocking 來禁止偏向鎖優(yōu)化反而可以提升性能。

二、lock的實現(xiàn)方案

  與synchronized不同的是lock是純java手寫的,與底層的JVM無關。在java.util.concurrent.locks包中有很多Lock的實現(xiàn)類,常用的有ReenTrantLock、ReadWriteLock(實現(xiàn)類有ReenTrantReadWriteLock)

,其實現(xiàn)都依賴java.util.concurrent.AbstractQueuedSynchronizer類(簡稱AQS),實現(xiàn)思路都大同小異,因此我們以ReentrantLock作為講解切入點。

分析之前我們先來花點時間看下AQS。AQS是我們后面將要提到的CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基礎,因此AQS也是Lock和Excutor實現(xiàn)的基礎。它的基本思想就是一個同步器,支持獲取鎖和釋放鎖兩個操作。

  iOS培訓,Swift培訓,蘋果開發(fā)培訓,移動開發(fā)培訓

  要支持上面鎖獲取、釋放鎖就必須滿足下面的條件:

  1、  狀態(tài)位必須是原子操作的

  2、  阻塞和喚醒線程

  3、  一個有序的隊列,用于支持鎖的公平性

  場景:可定時的、可輪詢的與可中斷的鎖獲取操作,公平隊列,或者非塊結構的鎖。

  主要從以下幾個特點介紹:

  1.可重入鎖

    如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配,而不是基于方法調用的分配。

  2.可中斷鎖

    可中斷鎖:顧名思義,就是可以相應中斷的鎖。

    在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。

    如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。

  3.公平鎖和非公平鎖

     公平鎖以請求鎖的順序來獲取鎖,非公平鎖則是無法保證按照請求的順序執(zhí)行。synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。而對于ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設置為公平鎖。

    參數(shù)為true時表示公平鎖,不傳或者false都是為非公平鎖。

ReentrantLock lock = new ReentrantLock(true);

  4.讀寫鎖

  讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

  正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發(fā)生沖突。

  ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口。

  可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

三、總結

  1.synchronized

  優(yōu)點:實現(xiàn)簡單,語義清晰,便于JVM堆棧跟蹤,加鎖解鎖過程由JVM自動控制,提供了多種優(yōu)化方案,使用更廣泛

  缺點:悲觀的排他鎖,不能進行高級功能

  2.lock

  優(yōu)點:可定時的、可輪詢的與可中斷的鎖獲取操作,提供了讀寫鎖、公平鎖和非公平鎖  

  缺點:需手動釋放鎖unlock,不適合JVM進行堆棧跟蹤

  3.相同點 

  都是可重入鎖