本文故事這是一個面試向的文章,因此我不會講得太過正統,爭取把枯燥的理論知識講得淺顯易懂一點。另外,我會盡可能把知識點整理得全面完整,因此篇幅會比較長。可是隻要你耐心看完,保證之後無論遇到什麼java鎖機制方面的問題,均可以玩玩全全遊刃有餘。下次再有面試官要拿鎖方面的問題來爲難你,嘿嘿,讓他當心點,別翻車。java
什麼是鎖?爲何要用鎖?只要有資源競爭,就須要有鎖。爲了引出鎖問題,先看下面一段簡單的代碼:面試
import java.util.concurrent.CountDownLatch; public class ThreadTest { private static final CountDownLatch countDown = new CountDownLatch(1); private static int m = 0; public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(() -> { try { countDown.await(); m++; System.out.println("m = " + m); } catch (InterruptedException e) { e.printStackTrace(); } },"thread"+i).start(); } countDown.countDown(); } }
這段代碼執行結果,三個線程都給m進行--操做,咱們期待是沒能打印出3,2,1的結果,可是執行後發現,打印出來的結果永遠不會是3,2,1.而會是3,2,2或者3,3,2這樣的。這就是由於多個線程共同競爭資源m形成的。而爲了不這種多線程環境下的資源競爭問題,就須要加入鎖。加鎖的方式大體有三種,一種是使用Atomic原子類,另外一種是使用可重入鎖ReentrantLock,這兩種是使用的同一個機制。另外一種是使用Synchronize關鍵字。下面會依次來整理一下這些鎖。算法
Atomic原子操做 和 ReentrantLock這裏能夠延伸出一個常常考的面試題: 給m 加上 volatile 關鍵字,能不能解決併發問題?數據庫
答案是NO,NO,NO。 volatile只是解決多線程及時可讀問題,並不能保證原子性。簡單理解就是volatile關鍵字只適用於少許(一個)線程寫,多個線程讀的場景。設計模式
把這兩種方式放在一塊兒,是由於這兩種方式都是經過java代碼搭配sun.misc.Unsafe中的本地調用實現的,屬於同一種鎖機制。數據結構
以原子操做爲例,原子操做是使用java.util.concurrent包下的AtomicXXX類進行原子操做。多線程
import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; public class ThreadTest { private static final CountDownLatch countDown = new CountDownLatch(1); private static AtomicInteger m = new AtomicInteger(0); private static int i = 0 ; public static void main(String[] args) { for (i = 0; i < 10; i++) { new Thread(() -> { try { countDown.await(); m.incrementAndGet(); System.out.println("m = " + m); } catch (InterruptedException e) { e.printStackTrace(); } },"thread"+i).start(); } countDown.countDown(); } }
這樣能夠保證每次執行的結果都是1~10(打印時間有前後,這個不用管)。那是怎麼作到的?固然是跟蹤代碼。一路跟蹤incrmentAndGet方法,會跟蹤到一個sun.mic.Unsafe類,這個是jdk的基礎包rt.jar中包含的一個類。裏面有一大堆這樣的方法。架構
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
這些方法都是native本地方法,是調用的JVM內的C++語言實現的。實現方法須要查看JDK源碼才能看獲得。可是這些方法有一個共同點,都是compareAndSwap開頭,很顯然,這就是他們的共同點, CAS算法。併發
CAS算法就是比較再交換的算法。爲了不線程在修改一個值的過程中被其餘線程給修改了,在修改一個內存的值時,先把值讀出來,爲E, 而後計算出值V(計算操做),在把V往原內存寫的時候,比較一下原內存地址的值是否和E相同, 相同就往回寫,不相同就再次重複。這樣能夠保證不會有線程共享值衝突。總體以下圖:分佈式
這個過程其實就引出了一個重要的概念,自旋鎖。即若是線程若是一直更新不成功,那線程就會一直不停去執行紅色的流程而不會中止。
這裏有兩個面試題:
一、ABA問題。就是在一次自旋過程當中,一個值通過屢次修改(值由A變成B,又由B又變成A,最終又變回原來的值,而CAS會認爲值沒有變,就會發生同步問題。實際上,在絕大部分狀況下,雖然中間過程被忽略了,可是隻要語義正確,並不會引發太大問題,而若是要完全解決ABA問題,能夠加個加版本號(AtomicStampedReference)或者加Boolean標誌(AtomicMarkableReference)來解決。
二、CAS算法自己並不能保證線程操做的原子性。在比較完,到更新值V以前,依然能夠被其餘線程修改。而JAVA的C++底層,最終仍是調用匯編指令lock申請了一個信號鎖,最終保持整個線程操做的原子性。因此CAS要保證原子性,仍是須要鎖,只是鎖已經轉移到了彙編級別。
基於這種機制,在JDK中還有不少相關的工具,如RetrantLock、ReadWriteLock等不少工具。 Atomic操做其實更多的細節在底層C++代碼中,對於咱們來講能看到的細節比較少。那別急,下面來仔細看看JDK提供的Synchronize關鍵字。
Synchronized關鍵字Synchronized關鍵字是經過一對字節碼彙編指令monitorenter/monitorexit來實現的,是JDK一開始就有的關鍵字。自JDK1.6之後,synchronized被進行了大幅度的優化,由重量級鎖改成了輕量級鎖,性能獲得極大的優化。所以,官方也開始建議優先使用Synchronized關鍵字來執行同步操做。
在瞭解Synchronized機制前,仍是先來個示例看看。先創建一個Maven工程,引入下面openjdk中的一個依賴來輔助咱們瞭解Synchronized的實現機制。
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
這個工具包能夠輔助打印出java對象的內存結果。先不用管細節,來一個簡單的示例,看看新鮮:
public static void main(String[] args) throws Exception{ Object o = new Object(); System.out.println("step 1: new Object"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized(o){ System.out.println("step 2: add synchronized"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }
下面來分析下執行結果: 首先看紅色部分,這裏帶出了java對象的一些內存結構。一個對象由16字節構成,前面12個都是object header,最後一個是補充字節,讓對象總體長度能夠被8整除。三個object header中,第一個叫作markword,相似一個對象標記位。第二個是class point,這個是對象的類指針,指向所屬的class類,固然,這是通過壓縮過的。第三個是成員變量的指針,固然,成員變量是能夠有多個的。
而後咱們看黃色部分,細節部分先不用管,可是經過比對上鎖先後的類佈局,咱們首先能夠獲得第一個結論:JVM鎖只做用在對象的markword位。簡單的說就是上鎖並不會改變對象自己,只是影響他的一個標誌位。
有了這個示例後,就能夠爲咱們後面的鑽牛角尖打個基礎了。
其實前面提到了一下,jdk1.6對synchronized的一個重大改進就是將其由重量級鎖改成了輕量級鎖。那就先來扯扯這兩種鎖狀態。
首先,在操做系統中,CPU有兩種運行狀態,一種是內核態,一種是用戶態。其中,內核態主要是運行操做系統程序的,能夠操做硬件,而用戶態就是主要用來運行用戶的應用程序的,不能夠直接操做硬件。有這樣的區分就是爲了防止用戶應用程序直接操做硬件,形成不可控的後果。
能夠想象若是有一個應用程序能夠把硬盤的引導區給格式化掉,那會是什麼情況?傳說在早期操做系統中,還真出現過這樣的應用程序。
而後,輕量級鎖就至關於用戶態的鎖,由應用程序本身控制,例如以前提到的自旋鎖。而重量級鎖,就至關於內核態的鎖,交由操做系統進行管理,例如咱們將java線程wait()後,實際上就至關因而上了重量級鎖。
把用戶程序比做一個房子,鎖就能夠比做房子內的一張門,門上的輕量級的鎖就至關於房主能夠本身開門關門,而重量級鎖就至關於找了小區物業來幫你管理這張門,開門關門都須要找物業申請。這個例子也就能夠用來理解爲何說jdk1.6後synchronized關鍵字的性能獲得了大幅度的提高。
輕量級鎖在必定條件下(鎖數量、CPU佔用率,能夠配置),會升級爲重量級鎖。由於輕量級鎖會一直不斷的自旋,而自旋顯然是要消耗系統資源的,當消耗到必定程度,固然就會想辦法減小自旋,這時就會升級爲重量級鎖。當鎖競爭升級爲重量級鎖後,就會中止自旋,而另外有機制讓線程進入休眠,等待喚醒。
通過前面的描述,就能夠有個大體的概念,輕量級鎖是將鎖對象的markword部分指向一個用戶空間裏的對象,而重量級鎖就是將鎖對象的markword部分指向一個操做系統內的數據結構。那顯然,他們競爭鎖的過程也是不一樣的。
重量級鎖競爭過程當中,被鎖對象的markwork會經過CAS操做嘗試更新爲一個包含了操做系統互斥量(mutex)和條件變量(condition variable)的指針。交由操做系統來控制對象的markword信息。
而輕量級鎖競爭過程纔是咱們關注的重點。JVM中線程競爭輕量級鎖的過程大體有一下幾步
這個鎖競爭過程能夠比喻爲不少人(線程)一塊兒上廁所搶蹲位(鎖對象)。每一個人上廁所時,會把本身的名牌貼在廁所門上,上完了廁所再把名牌拿走。後面的人看到門上有人了,就出去轉轉,等下再來。若是下次回來門上的名牌沒了,他就上。可是若是出去轉了不少次,廁所仍是沒空出來,那他就沒辦法了,只能去找物業幫忙協調找廁所蹲位了。
在JDK1.6後,JDK在實踐中發現一種假設,即大多數synchronized競爭狀況下,其實只有一個線程在運行。因而,JVM在實際中又加入了另外一種更輕量的鎖,叫作偏向鎖。因此JVM中的鎖機制有以下三種:
偏向鎖(Biased Lock)>輕量級鎖(Lightweight Lock)>重量級鎖(Heavyweight Lock)
偏向鎖也是屬於一種輕量級鎖。這三種機制的切換是根據資源競爭激烈程度進行的。在幾乎無競爭的條件下,會使用偏向鎖。當競爭漸趨激烈,會升級爲輕量級鎖。當競爭過於激烈,就會升級爲重量級鎖。JVM中鎖升級的過程以下圖: 看上圖,升級的過程大體都已經比較清楚了,那其中有一個奇怪的文字,偏向鎖未啓動,這是什麼意思呢?
這裏涉及到一個面試常常要問的問題:打開偏向鎖必定可以提高性能嗎?爲何?
既然這麼問,答案確定是否了。爲何呢?在某些明知資源競爭很是激烈的狀況下(例如應用啓動過程,須要加載大量的資源,確定會有很是激烈的資源競爭),若是還要打開偏向鎖,那廁所門上貼名牌撕名牌的過程也會至關頻繁,這也會消耗至關多的資源。這時,跳過偏向鎖,直接升級爲輕量級鎖,更能節省資源。
實際上, JVM中有兩個跟偏向鎖有關的啓動參數:
-XX:-UseBiasedLocking 啓動偏向鎖
-XX:BiasedLockingStartupDelay=0 偏向鎖啓動延遲。默認值是4秒,即JVM啓動4秒左右後纔會打開偏向鎖。
這裏首先插入一個小知識,就是HotSpot的JAVA對象內存佈局。總體以下圖所示。其中紅框的標誌位部分就是對象的鎖狀態。如今看不懂不要緊,咱們結合這個表和下面的示例就能看明白這個鎖標誌了。先看最後兩位,若是是01(偏向鎖),那就再往前看一位。 既然有配置,那就要驗證一下。咱們用下面這個簡單的例子來演示一下。
public static void main(String[] args) throws Exception{ Object o = new Object(); System.out.println("object 1 no biasedLock"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); Thread.sleep(5000);//休眠時間超過-XX:BiasedLockingStartupDelay默認值4秒 Object o2 = new Object(); System.out.println("object 2 biasedLock opened"); System.out.println(ClassLayout.parseInstance(o2).toPrintable()); }
咱們來看下執行結果:(看到這裏,記得回頭再看看上面加了synchronized關鍵字後的對象佈局狀況) JVM的鎖機制到這裏,大體就已經弄完了。把這些細節搞明白了,至少java面試官就不用怕了吧。
最後
一直想整理出一份完美的面試寶典,可是時間上一直騰不開,這套一千多道面試題寶典,結合今年金三銀四各類大廠面試題,以及 GitHub 上 star 數超 30K+ 的文檔整理出來的,我上傳之後,毫無心外的短短半個小時點贊量就達到了 13k,說實話仍是有點難以想象的。
內容涵蓋:Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、Redis、MySQL、Spring、SpringBoot、SpringCloud、RabbitMQ、Kafka、Linux等技術棧(485頁)
內容涵蓋:Java基礎、JVM、高併發、多線程、分佈式、設計模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、數據庫、雲計算等
因爲篇幅限制,詳解資料太全面,細節內容太多,因此只把部分知識點截圖出來粗略的介紹,每一個小節點裏面都有更細化的內容!
須要的小夥伴,能夠一鍵三連,點擊這裏獲取免費領取方式!