1. 程式人生 > >JAVA併發程式設計:synchronized關鍵字深入解析

JAVA併發程式設計:synchronized關鍵字深入解析

生活

天氣賊好的一個禮拜二的吃完晚飯的晚上。
他們去聽課了。
不想寫程式碼。
我在這看點東西吧~

閒談

對於synchronized的記憶是最早對同步的概念。那時候聊到同步,就會說到StringBuilder和StringBuffer,裡面 的方法都是一樣的,但是StringBuffer裡的方法都加了synchronized來修飾,所以是執行緒安全的。
這個關鍵字在JDK1.6以前還是挺笨重的,效能很差,在1.6以後做了很多優化,引入了偏向鎖、輕量級鎖、重量級鎖的概念,效能上已經和ReentrantLock不分上下了。所以今天就來講講這個JVM語義上的互斥鎖。

本章的學習目的

1、物件頭
2、鎖優化

場景

下面來一個生活中的實際場景,多執行緒的1000人搶1000張票。
前面我們講到volatile關鍵字起到可見性的作用,所以這裡直接用volatile來修飾共享變數,看看會有啥效果呢?
下面直接上案例程式碼:

public class Test {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(100);
        for(int i=0;i<100;i++){
            new Thread(new BuyTicket(latch)).start();
        }
        latch.await();
        System.out.println(BuyTicket.ticket);
    }

    static class BuyTicket implements Runnable {
        public static volatile  int ticket = 100;
        private CountDownLatch latch;

        @Override
        public void run() {
            
                try {
                    Thread.sleep(5L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                latch.countDown();

        }

        public BuyTicket(CountDownLatch latch) {
            this.latch = latch;
        }
    }
}

每次執行後得到的餘票都比0大,說明volatile這個關鍵字本身不是執行緒安全的。
需要加鎖實現執行緒安全:

synchronized(BuyTicket.class){
  try {
                    Thread.sleep(5L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
                latch.countDown();
}

加了鎖以後會發現明細的卡慢,並且經過多次實現得到的餘票都是0,說明用synchronized來實現執行緒安全是可行的。

使用

1、synchronized 修飾靜態方法,加鎖物件是 類名.class物件
2、synchronized 修飾非靜態方法,加鎖物件this
3、synchronized修飾 程式碼塊,加鎖物件即括號裡的物件。

原理(反編譯)

1、
關於synchronized的實現原理,要從兩個命令說起,先來看下上述BuyTicket反編譯的結果:
在這裡插入圖片描述
通過synchronized 修飾同步程式碼塊,反編譯後主要關注以上圈紅的命令。
關於monitor:每個物件都有一個物件監視器,monitor;當monitor被佔用時物件就處於鎖定狀態。
1.1 monitorenter
執行緒通過執行monitorenter指令嘗試佔用monitor物件的過程如下:
a:如果monitor的進入數為0,則該執行緒進入monitor,進入數+1,此時該執行緒佔用monitor,即成為該monitor的所有者。
b:如果該執行緒已經佔用monitor,只是重新進入,只需要monitor+1即可
c:如果其他執行緒已經佔用monitor,則該執行緒進入阻塞狀態,直到monitor進入數減為0,才會嘗試去佔用monitor.
1.2 monitorexit
執行monitorexit的執行緒必須是對應的monitor所有者執行緒。
執行monitorexit時,monitor進入數-1,直到進入數減為0,這個執行緒退出monitor,不再是這個monitor的所有者。其他被阻塞的執行緒將被喚醒參與monitor的競爭佔用。

注意1:
可以看到上面反編譯的結果裡有兩個monitorexit,第二個monitorexit其實是為了在出現異常時退出monitor使用的。
注意2:
synchronized依賴於monitor來實現同步,Object中的wait和notify其實也是依賴於monitor實現的,所以當不在同步塊執行這兩個方法時會報錯:
java.lang.IllegalMonitorStateException

2、上面是同步程式碼塊的反編譯,下面來看下同步方法的反編譯結果:
先來非靜態同步方法:

 public synchronized void  say(){
        System.out.println("hello");
    }

反編譯結果如下
在這裡插入圖片描述
可以看到在非靜態同步方法下,反編譯後,發現非靜態同步方法通過對ACC_SYNCHRONIZED的設定標誌是否同步。
在執行方法前,指定會先查詢ACC_SYNCHRONIZED是否被標記,如果被標記,則嘗試獲取monitor,獲取成功後執行方法體,執行完畢退出monitor。方法執行期間,其他執行緒無法獲取monitor.

靜態同步方法是一樣的,無非是鎖定的物件為類.class,還多個標誌 ACC_STATIC,代表靜態。

物件頭

物件頭也是個很重要的概念,記得第一次聽到他還是半年前,那個時候面試的時候,問到synchronized的時候,面試官聊到這個物件頭的時候就一臉懵逼了。

前面講到同步的時候,要先獲取佔用物件的monitor,也就是獲取物件的鎖,那這個monitor是什麼呢、這個monitor其實就是一個物件的標記,這個標記就是存在物件的物件頭裡的。
物件頭裡存放了物件的hashcode,分代年齡,鎖標記位。

JDK1.6下,鎖一共有四種狀態,分別是無鎖狀態、偏向鎖、輕量級鎖、重量級鎖。
這幾種狀態隨著競爭激烈程度而隨之升級。
鎖可以升級但是不能降級,也就是說輕量級鎖不能降級為偏向鎖,之所以這麼設計的原因是為了提高獲取鎖和釋放鎖的效率。

在這裡插入圖片描述

偏向鎖、輕量級鎖、重量級鎖

偏向鎖

偏向鎖獲取:
當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀的鎖記錄裡儲存鎖偏向的執行緒ID,
以後該執行緒進入和退出同步塊不需要通過cas來加鎖和釋放鎖,只需測試物件頭裡markword裡是否儲存了當前執行緒的鎖偏向ID,
如果測成功,則說明已經獲取鎖。
如果測試不成功,則判斷鎖標記位是否為1【代表偏向鎖】。
如果不是1,則嘗試cas競爭鎖,
如果是1,則嘗試通過cas將物件頭的偏向鎖指向當前執行緒。

偏向鎖撤銷:
偏向鎖使用了一種等到競爭出現才會釋放鎖的機制。所以當其他執行緒嘗試獲取鎖時,持有偏向鎖的執行緒才會釋放。

偏向鎖的撤銷,需要等待全域性安全點(即沒有位元組碼執行)。
首先暫停持有偏向鎖的執行緒,然後判斷這個執行緒是否活著,如果已經死亡就直接設定物件頭為無鎖。
如果還活著,擁有偏向鎖的棧會執行。
棧中的鎖記錄和物件頭裡的markword要麼偏向於其他執行緒,要麼設定為無鎖或者不適用偏向鎖,升級為輕量級鎖。最後喚醒暫停的執行緒。

如果關閉偏向鎖:
偏向鎖在Java 6和Java 7裡是預設啟用的,但是它在應用程式啟動幾秒鐘之後才啟用,如有必要可以使用JVM引數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程式裡所有的鎖通常情況下處於競爭狀態,可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式預設會進入輕量級鎖狀態

輕量級鎖

加鎖:
執行緒在執行同步塊之前,JVM會在當前執行緒的棧幀中建立存放鎖記錄的空間,並將物件頭中的markword複製過來。然後嘗試將物件頭裡的mark word指向鎖記錄的指標。如果成功,執行緒就獲得鎖,如果失敗,執行緒將cas自旋嘗試。

解鎖:
輕量級鎖解鎖時,會cas嘗試把執行緒棧幀中的鎖記錄(Displaced Mark Word)替換到物件頭。如果成功,說明沒加競爭。如果失敗,說明存在鎖競爭,鎖就會膨脹成重量級鎖。

重量級鎖

輕量級鎖一旦升級為重量級鎖,就不能降級。
此時當有一個執行緒獲得鎖進入同步快執行時,其他執行緒都只能阻塞在同步快外面,等待這個執行緒執行完畢,釋放鎖,喚醒等待的執行緒去競爭。

後記

洗洗睡覺