前言
????????公司加班太狠了,都沒啥時間充電,這周終于結束了。這次整理了Java并發編程里面的synchronized關鍵字,又稱為隱式鎖,與JUC包中的Lock顯示鎖相對應;這個關鍵字從Java誕生開始就有,稱之為重量級鎖,自從JDK1.6之后官方對該關鍵字進行優化,引入了輕量級鎖和偏向鎖,于是就有了鎖升級的概念。
使用
在代碼中使用這個關鍵字總共有以下三種:
private static Object object = new Object();private synchronized void function1() {//鎖住當前實例對象}private static synchronized void function2() {//鎖住當前class對象,可以認為是鎖住當前Class文件}public static void main( String[] args ) {synchronized (object) {//鎖住object對象}}
1:普通方法同步;
2:靜態方法同步;
3:同步代碼塊括號中的對象;
使用synchronized關鍵字進行同步,則鎖是儲存于Java對象頭里面的Mark Word。
Java對象頭里面的Mark Word里面主要是存儲對象的hashCode、分代年齡(用于判斷為老年代還是年輕代,在垃圾回收器里面用得到)以及鎖標記位。
鎖的升級
在JDK1.6之后,引入了引入了偏向鎖和輕量級鎖的狀態,目的是為了提升鎖的釋放和獲取效率,減少性能的開銷,所以synchronized就有四種級別鎖的狀態,級別從低到高分別是無鎖狀態、偏向鎖狀態、輕量級鎖以及重量級鎖狀態;四種狀態實質上是對象頭儲存的鎖標記位不一致,使用CAS更改對象頭的標記位進行鎖狀態位;并且在記錄鎖的標記位的同時,也會在Mark Word里面記錄鎖線程的ID
無鎖
既在對象頭的Mark Word沒有標記鎖狀態的時候就是無鎖狀態
偏向鎖
單個線程進行訪問或調用帶synchronized的同步代碼塊或方法時,會在先判斷對象頭里面鎖標志位是否有線程ID,如果沒有線程ID的話,將當前線程ID寫入進去,并且也會在棧幀中的鎖記錄里面進行記錄。此時,鎖的狀態位為偏向鎖,通俗來說可以說是只要單線程訪問同步代碼塊,從無鎖狀態就會便成為偏向鎖狀態;如果在對象頭里面存在線程ID的話,如果當前線程ID是與對象頭里面記錄的線程ID的話,那么就可以直接訪問,不需要使用CAS去進行競爭鎖了;不一致的話,那么就會使用CAS進行競爭鎖。
當其他線程開始競爭偏向鎖的時候,那么持有偏向鎖的線程就去會釋放偏向鎖,供其他線程使用;這時候就出現一種偏向鎖的撤銷概念
偏向鎖的撤銷
偏向鎖的撤銷就是,會先暫停持有偏向鎖的線程,然后去對象頭里面判斷記錄線程ID的線程是否還在處于活動狀態,如果處于非活動狀態,那么就會將Mark Word里面的鎖標志位設置為無鎖狀態;如果處于活動狀態的話,會先遍歷偏向對象的鎖記錄、棧里面鎖記錄以及對象頭里面Mark Word里面的鎖標記位,并且將對象頭中鎖標志位設置為無鎖狀態或者升級成輕量鎖狀態的,然后喚醒持有偏向鎖的的線程,繼續執行;
輕量級鎖
加鎖
當執行同步代碼塊升級為輕量級鎖的時候,會在棧幀中創建一塊用于儲存鎖記錄的空間,并且將對象頭里面Mark Word復制到記錄中(Displaced Mark Word),線程開始會使用CAS將Mark Word替換為指向鎖記錄的指針,如果獲取成功,那么當前線程獲取鎖,如果失敗,采用自旋來獲取鎖,如果自旋獲取鎖失敗,那么會膨脹為重量級鎖。
解鎖
輕量級鎖解鎖會使用CAS將Displaced Mark Word替換回到對象頭中,如果成功了,表示沒有鎖競爭;如果失敗了表示有鎖在競爭,那么就會膨脹成重量級鎖,那么在自旋的獲取鎖的線程就會進行線程阻塞;
由于自旋會大量消耗CPU資源,所以一旦升級成為了重量級鎖之后,那么就不會進行降級了。
重量級鎖
這個就是線程阻塞了,基本上可以和Lock表現一致了,一旦有線程獲取鎖,其他獲取鎖的線程將會阻塞,釋放鎖之后將會喚醒阻塞線程去競爭鎖
鎖 | 優點 | 缺點 | 使用場景 |
偏向鎖 | 加鎖和解鎖不需要額外的資源消耗,性能快 | 如果線程之間存在鎖競爭,那么會出現鎖撤銷,暫停線程,比較消耗資源 | 適用于單線程使用場景 |
輕量級鎖 | 線程不會阻塞,提高響應程度 | 自旋消耗CPU性能,容易升級為重量級鎖 | 適用于同步代碼塊執行非常快的 |
重量級鎖 | 線程不會自旋,避免過多消耗CPU資源 | 線程阻塞,響應時間緩慢 | 提高吞吐量,同步代碼塊執行較長 |
等待/通知機制
這個之前是有篇講過線程之間的共享協作:線程協作?這個里面提到過如何使用
現在看看底層是如何運行的執行,我們先看一段代碼
public class SynchronziedDemo {public static Object object = new Object();public static void main(String[] args) {synchronized (object) {}m();}public static synchronized void m() {}
}
將這段代碼編譯后使用javap -v命令進行反編譯
會得到class文件的編譯后的c代碼:
同步代碼塊使用的事monitorenter(獲取鎖)和monitorexit(釋放鎖)指令,同步放上是使用ACC_SYNCRONIZED來完成的。
無論是哪個本質上其實是個monitor監視器進行完成的,每個對象都會有一個監視器,線程需先獲取到monitor監視器才能訪問同步代碼塊或者方法,而沒有獲取到的線程就會進入自旋,升級為重量級鎖,然后會進入到阻塞狀態,這時候會有一個同步隊列(SynchronizedQueue),阻塞的線程會加入到這個隊列里面,等待獲取到監視器的線程調用monitorexit指定后,會喚醒同步隊列里面的等待線程。
等待/通知機制里面對了一個WaitQueue即等待隊列,案例:
public class WaitNotify {public static Object object = new Object();public static void main(String[] args) {new Thread(new WaitClass()).start();new Thread(new NotifyClass()).start();}static class NotifyClass implements Runnable {@Overridepublic void run() {synchronized (object) {try {TimeUnit.SECONDS.sleep(2L);System.out.println(Thread.currentThread() + "notify start...");object.notifyAll();System.out.println(Thread.currentThread() + "notify end...");} catch (InterruptedException e) {e.printStackTrace();}}}}static class WaitClass implements Runnable {@Overridepublic void run() {synchronized (object) {System.out.println(Thread.currentThread() + "wait start....");try {object.wait();System.out.println(Thread.currentThread() + "wait end....");} catch (InterruptedException e) {e.printStackTrace();}}}}
}
?打印日志如下:
Thread[Thread-0,5,main]wait start....
Thread[Thread-1,5,main]notify start...
Thread[Thread-1,5,main]notify end...
Thread[Thread-0,5,main]wait end....
對于wait線程獲取到object對象的監視器之后,調用wait方法后進入等待隊列(WaitQueue),然后釋放監視器。
對于notify線程來說,先獲取到object對象監視器之后,然后調用notifyAll方法,將WaitQueue里面的所有等待線程同步到同步隊列中,(如果是調用notify方法就會值同步一個線程并非所有線程),然后釋放監視器,就會喚醒同步列隊中的線程。
對于wait線程,在同步隊列唄喚醒后,會重新獲取監視器,然后繼續執行wait方法后面的代碼。
這樣就完成一個等待通知的機制了。