Synchronized ,其原理是什麼?java
Synchronized 是由 JVM 實現的一種實現互斥同步的一種方式,若是你查看被 Synchronized 修飾過的程序塊編譯後的字節碼,會發現,被 Synchronized 修飾過的程序塊,在編譯先後被編譯器生成了 monitorenter 和 monitorexit 兩個字節碼指令程序員
這兩個指令是什麼意思呢?算法
在虛擬機執行到 monitorenter 指令時,首先要嘗試獲取對象的鎖:數組
若是這個對象沒有鎖定,或者當前線程已經擁有了這個對象的鎖,把鎖的計數器 +1;當執行 monitorexit 指令時將鎖計數器 -1;當計數器爲 0 時,鎖就被釋放了。安全
若是獲取對象失敗了,那當前線程就要阻塞等待,直到對象鎖被另一個線程釋放爲止。數據結構
Java 中 Synchronize 經過在對象頭設置標記,達到了獲取鎖和釋放鎖的目的。多線程
問題二:你剛纔提到獲取對象的鎖,這個「鎖」究竟是什麼?如何肯定對象的鎖?併發
「鎖」的本質實際上是 monitorenter 和 monitorexit 字節碼指令的一個 Reference 類型的參數,即要鎖定和解鎖的對象。咱們知道,使用 Synchronized 能夠修飾不一樣的對象,所以,對應的對象鎖能夠這麼肯定。框架
若是 Synchronized 明確指定了鎖對象,好比 Synchronized(變量名)、Synchronized(this) 等,說明加解鎖對象爲該對象。工具
若是沒有明確指定:
若 Synchronized 修飾的方法爲非靜態方法,表示此方法對應的對象爲鎖對象;
若 Synchronized 修飾的方法爲靜態方法,則表示此方法對應的類對象爲鎖對象。
注意,當一個對象被鎖住時,對象裏面全部用 Synchronized 修飾的方法都將產生堵塞,而對象裏非 Synchronized 修飾的方法可正常被調用,不受鎖影響。
可重入性是鎖的一個基本要求,是爲了解決本身鎖死本身的狀況。
好比下面的僞代碼,一個類中的同步方法調用另外一個同步方法,假如 Synchronized 不支持重入,進入 method2 方法時當前線程得到鎖,method2 方法裏面執行 method1 時當前線程又要去嘗試獲取鎖,這時若是不支持重入,它就要等釋放,把本身阻塞,致使本身鎖死本身。
對 Synchronized 來講,可重入性是顯而易見的,剛纔提到,在執行 monitorenter 指令時,若是這個對象沒有鎖定,或者當前線程已經擁有了這個對象的鎖(而不是已擁有了鎖則不能繼續獲取),就把鎖的計數器 +1,其實本質上就經過這種方式實現了可重入性。
問題四:JVM 對 Java 的原生鎖作了哪些優化?
在 Java 6 以前,Monitor 的實現徹底依賴底層操做系統的互斥鎖來實現,也就是咱們剛纔在問題二中所闡述的獲取/釋放鎖的邏輯。
因爲 Java 層面的線程與操做系統的原生線程有映射關係,若是要將一個線程進行阻塞或喚起都須要操做系統的協助,這就須要從用戶態切換到內核態來執行,這種切換代價十分昂貴,很耗處理器時間,現代 JDK 中作了大量的優化。
一種優化是使用自旋鎖,即在把線程進行阻塞操做以前先讓線程自旋等待一段時間,可能在等待期間其餘線程已經解鎖,這時就無需再讓線程執行阻塞操做,避免了用戶態到內核態的切換。
現代 JDK 中還提供了三種不一樣的 Monitor 實現,也就是三種不一樣的鎖:
偏向鎖(Biased Locking)
輕量級鎖
重量級鎖
這三種鎖使得 JDK 得以優化 Synchronized 的運行,當 JVM 檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這就是鎖的升級、降級。
當沒有競爭出現時,默認會使用偏向鎖。
JVM 會利用 CAS 操做,在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖,由於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖能夠下降無競爭開銷。
若是有另外一線程試圖鎖定某個被偏斜過的對象,JVM 就撤銷偏斜鎖,切換到輕量級鎖實現。
輕量級鎖依賴 CAS 操做 Mark Word 來試圖獲取鎖,若是重試成功,就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。
問題五:爲何說 Synchronized 是非公平鎖?
非公平主要表如今獲取鎖的行爲上,並不是是按照申請鎖的時間先後給等待線程分配鎖的,每當鎖被釋放後,任何一個線程都有機會競爭到鎖,這樣作的目的是爲了提升執行性能,缺點是可能會產生線程飢餓現象。
問題六:什麼是鎖消除和鎖粗化?
鎖消除:指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但被檢測到不可能存在共享數據競爭的鎖進行消除。主要根據逃逸分析。
程序員怎麼會在明知道不存在數據競爭的狀況下使用同步呢?不少不是程序員本身加入的。
鎖粗化:原則上,同步塊的做用範圍要儘可能小。可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做在循環體內,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
鎖粗化就是增大鎖的做用域。
Synchronized 顯然是一個悲觀鎖,由於它的併發策略是悲觀的:
無論是否會產生競爭,任何的數據操做都必需要加鎖、用戶態核心態轉換、維護鎖計數器和檢查是否有被阻塞的線程須要被喚醒等操做。
隨着硬件指令集的發展,咱們可使用基於衝突檢測的樂觀併發策略。先進行操做,若是沒有其餘線程徵用數據,那操做就成功了;
若是共享數據有徵用,產生了衝突,那就再進行其餘的補償措施。這種樂觀的併發策略的許多實現不須要線程掛起,因此被稱爲非阻塞同步。
樂觀鎖的核心算法是 CAS(Compareand Swap,比較並交換),它涉及到三個操做數:內存值、預期值、新值。當且僅當預期值和內存值相等時纔將內存值修改成新值。
這樣處理的邏輯是,首先檢查某塊內存的值是否跟以前我讀取時的同樣,如不同則表示期間此內存值已經被別的線程更改過,捨棄本次操做,不然說明期間沒有其餘線程對此內存值操做,能夠把新值設置給此塊內存。
CAS 具備原子性,它的原子性由 CPU 硬件指令實現保證,即便用 JNI 調用 Native 方法調用由 C++ 編寫的硬件級別指令,JDK 中提供了 Unsafe 類執行這些操做。
問題八:樂觀鎖必定就是好的嗎?
樂觀鎖避免了悲觀鎖獨佔對象的現象,同時也提升了併發性能,但它也有缺點:
樂觀鎖只能保證一個共享變量的原子操做。若是多一個或幾個變量,樂觀鎖將變得力不從心,但互斥鎖能輕易解決,無論對象數量多少及對象顆粒度大小。
長時間自旋可能致使開銷大。假如 CAS 長時間不成功而一直自旋,會給 CPU 帶來很大的開銷。
ABA 問題。CAS 的核心思想是經過比對內存值與預期值是否同樣而判斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是 A,後來被一條線程改成 B,最後又被改爲了 A,則 CAS 認爲此內存值並無發生改變,但其實是有被其餘線程改過的,這種狀況對依賴過程值的情景的運算結果影響很大。解決的思路是引入版本號,每次變量更新都把版本號加一
問題一:跟 Synchronized 相比,可重入鎖 ReentrantLock 其實現原理有什麼不一樣?
其實,鎖的實現原理基本是爲了達到一個目的:
讓全部的線程都能看到某種標記。
Synchronized 經過在對象頭中設置標記實現了這一目的,是一種 JVM 原生的鎖實現方式,而 ReentrantLock 以及全部的基於 Lock 接口的實現類,都是經過用一個 volitile 修飾的 int 型變量,並保證每一個線程都能擁有對該 int 的可見性和原子修改,其本質是基於所謂的 AQS 框架。
問題二:那麼請談談 AQS 框架是怎麼回事兒?
AQS(AbstractQueuedSynchronizer 類)是一個用來構建鎖和同步器的框架,各類 Lock 包中的鎖(經常使用的有 ReentrantLock、ReadWriteLock),以及其餘如 Semaphore、CountDownLatch,甚至是早期的 FutureTask 等,都是基於 AQS 來構建。
AQS 在內部定義了一個 volatile int state 變量,表示同步狀態:當線程調用 lock 方法時 ,若是 state=0,說明沒有任何線程佔有共享資源的鎖,能夠得到鎖並將 state=1;若是 state=1,則說明有線程目前正在使用共享變量,其餘線程必須加入同步隊列進行等待。
AQS 經過 Node 內部類構成的一個雙向鏈表結構的同步隊列,來完成線程獲取鎖的排隊工做,當有線程獲取鎖失敗後,就被添加到隊列末尾。
Node 類是對要訪問同步代碼的線程的封裝,包含了線程自己及其狀態叫 waitStatus(有五種不一樣 取值,分別表示是否被阻塞,是否等待喚醒,是否已經被取消等),每一個 Node 結點關聯其 prev 結點和 next 結點,方便線程釋放鎖後快速喚醒下一個在等待的線程,是一個 FIFO 的過程。
Node 類有兩個常量,SHARED 和 EXCLUSIVE,分別表明共享模式和獨佔模式。所謂共享模式是一個鎖容許多條線程同時操做(信號量 Semaphore 就是基於 AQS 的共享模式實現的),獨佔模式是同一個時間段只能有一個線程對共享資源進行操做,多餘的請求線程須要排隊等待(如 ReentranLock)。
AQS 經過內部類 ConditionObject 構建等待隊列(可有多個),當 Condition 調用 wait() 方法後,線程將會加入等待隊列中,而當 Condition 調用 signal() 方法後,線程將從等待隊列轉移動同步隊列中進行鎖競爭。
AQS 和 Condition 各自維護了不一樣的隊列,在使用 Lock 和 Condition 的時候,其實就是兩個隊列的互相移動。
問題三:請儘量詳盡地對比下 Synchronized 和 ReentrantLock 的異同。
ReentrantLock 是 Lock 的實現類,是一個互斥的同步鎖。
從功能角度,ReentrantLock 比 Synchronized 的同步操做更精細(由於能夠像普通對象同樣使用),甚至實現 Synchronized 沒有的高級功能,如:
等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,對處理執行時間很是長的同步塊頗有用。
帶超時的獲取鎖嘗試:在指定的時間範圍內獲取鎖,若是時間到了仍然沒法獲取則返回。
能夠判斷是否有線程在排隊等待獲取鎖。
能夠響應中斷請求:與 Synchronized 不一樣,當獲取到鎖的線程被中斷時,可以響應中斷,中斷異常將會被拋出,同時鎖會被釋放。
能夠實現公平鎖。
從鎖釋放角度,Synchronized 在 JVM 層面上實現的,不但能夠經過一些監控工具監控 Synchronized 的鎖定,並且在代碼執行出現異常時,JVM 會自動釋放鎖定;可是使用 Lock 則不行,Lock 是經過代碼實現的,要保證鎖定必定會被釋放,就必須將 unLock() 放到 finally{} 中。
從性能角度,Synchronized 早期實現比較低效,對比 ReentrantLock,大多數場景性能都相差較大。
可是在 Java 6 中對其進行了很是多的改進,在競爭不激烈時,Synchronized 的性能要優於 ReetrantLock;在高競爭狀況下,Synchronized 的性能會降低幾十倍,可是 ReetrantLock 的性能能維持常態。
問題四:ReentrantLock 是如何實現可重入性的?
ReentrantLock 內部自定義了同步器 Sync(Sync 既實現了 AQS,又實現了 AOS,而 AOS 提供了一種互斥鎖持有的方式),其實就是加鎖的時候經過 CAS 算法,將線程對象放到一個雙向鏈表中,每次獲取鎖的時候,看下當前維護的那個線程 ID 和當前請求的線程 ID 是否同樣,同樣就可重入了。
問題五:除了 ReetrantLock,你還接觸過 JUC 中的哪些併發工具?
一般所說的併發包(JUC)也就是 java.util.concurrent 及其子包,集中了 Java 併發的各類基礎工具類,具體主要包括幾個方面:
提供了 CountDownLatch、CyclicBarrier、Semaphore 等,比 Synchronized 更加高級,能夠實現更加豐富多線程操做的同步結構。
提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者經過相似快照機制實現線程安全的動態數組 CopyOnWriteArrayList 等,各類線程安全的容器。
提供了 ArrayBlockingQueue、SynchorousQueue 或針對特定場景的 PriorityBlockingQueue 等,各類併發隊列實現。
強大的 Executor 框架,能夠建立各類不一樣類型的線程池,調度任務運行等。
問題六:請談談 ReadWriteLock 和 StampedLock。
雖然 ReentrantLock 和 Synchronized 簡單實用,可是行爲上有必定侷限性,要麼不佔,要麼獨佔。實際應用場景中,有時候不須要大量競爭的寫操做,而是以併發讀取爲主,爲了進一步優化併發操做的粒度,Java 提供了讀寫鎖。
讀寫鎖基於的原理是多個讀操做不須要互斥,若是讀鎖試圖鎖定時,寫鎖是被某個線程持有,讀鎖將沒法得到,而只好等待對方操做結束,這樣就能夠自動保證不會讀取到有爭議的數據。
ReadWriteLock 表明了一對鎖,下面是一個基於讀寫鎖實現的數據結構,當數據量較大,併發讀多、併發寫少的時候,可以比純同步版本凸顯出優點:
讀寫鎖看起來比 Synchronized 的粒度彷佛細一些,但在實際應用中,其表現也並不盡如人意,主要仍是由於相對比較大的開銷。
因此,JDK 在後期引入了 StampedLock,在提供相似讀寫鎖的同時,還支持優化讀模式。優化讀基於假設,大多數狀況下讀操做並不會和寫操做衝突,其邏輯是先試着修改,而後經過 validate 方法確認是否進入了寫模式,若是沒有進入,就成功避免了開銷;若是進入,則嘗試獲取讀鎖。
問題七:如何讓 Java 的線程彼此同步?你瞭解過哪些同步器?請分別介紹下。
JUC 中的同步器三個主要的成員:CountDownLatch、CyclicBarrier 和 Semaphore,經過它們能夠方便地實現不少線程之間協做的功能。
CountDownLatch 叫倒計數,容許一個或多個線程等待某些操做完成。看幾個場景:
跑步比賽,裁判須要等到全部的運動員(「其餘線程」)都跑到終點(達到目標),才能去算排名和頒獎。
模擬併發,我須要啓動 100 個線程去同時訪問某一個地址,我但願它們能同時併發,而不是一個一個的去執行。
用法:CountDownLatch 構造方法指明計數數量,被等待線程調用 countDown 將計數器減 1,等待線程使用 await 進行線程等待。一個簡單的例子:
CyclicBarrier 叫循環柵欄,它實現讓一組線程等待至某個狀態以後再所有同時執行,並且當全部等待線程被釋放後,CyclicBarrier 能夠被重複使用。CyclicBarrier 的典型應用場景是用來等待併發線程結束。
CyclicBarrier 的主要方法是 await(),await() 每被調用一次,計數便會減小 1,並阻塞住當前線程。當計數減至 0 時,阻塞解除,全部在此 CyclicBarrier 上面阻塞的線程開始運行。
在這以後,若是再次調用 await(),計數就又會變成 N-1,新一輪從新開始,這即是 Cyclic 的含義所在。CyclicBarrier.await() 帶有返回值,用來表示當前線程是第幾個到達這個 Barrier 的線程。
舉例說明以下:
Semaphore,Java 版本的信號量實現,用於控制同時訪問的線程個數,來達到限制通用資源訪問的目的,其原理是經過 acquire() 獲取一個許可,若是沒有就等待,而 release() 釋放一個許可。
若是 Semaphore 的數值被初始化爲 1,那麼一個線程就能夠經過 acquire 進入互斥狀態,本質上和互斥鎖是很是類似的。可是區別也很是明顯,好比互斥鎖是有持有者的,而對於 Semaphore 這種計數器結構,雖然有相似功能,但其實不存在真正意義的持有者,除非咱們進行擴展包裝。
問題八:CyclicBarrier 和 CountDownLatch 看起來很類似,請對比下呢?
它們的行爲有必定類似度,區別主要在於:
CountDownLatch 是不能夠重置的,因此沒法重用,CyclicBarrier 沒有這種限制,能夠重用。
CountDownLatch 的基本操做組合是 countDown/await,調用 await 的線程阻塞等待 countDown 足夠的次數,無論你是在一個線程仍是多個線程裏 countDown,只要次數足夠便可。 CyclicBarrier 的基本操做組合就是 await,當全部的夥伴都調用了 await,纔會繼續進行任務,並自動進行重置。
CountDownLatch 目的是讓一個線程等待其餘 N 個線程達到某個條件後,本身再去作某個事(經過 CyclicBarrier 的第二個構造方法 public CyclicBarrier(int parties, Runnable barrierAction),在新線程裏作事能夠達到一樣的效果)。而 CyclicBarrier 的目的是讓 N 多線程互相等待直到全部的都達到某個狀態,而後這 N 個線程再繼續執行各自後續(經過 CountDownLatch 在某些場合也能完成相似的效果)。