并發編程學習四、synchronized底層-并發編程的實現原理

Valentine 轉載請標明出處。

synchronized的使用
在多線程并發編程中synchronized一直是元老級的角色,很多人都會稱呼它為重量級鎖。但是隨著Java SE 1.6對synchronized進行了各種優化之后,有些情況下它就并不那么重了,Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,以及鎖的存儲結構和升級。
synchronized的使用示例

public class Demo {
    private static int count = 0;
    private static int count1 = 0;
    private static int count2 = 0;

    private static void inc() {
        synchronized (Demo.class) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    }

    private synchronized void inc1() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count1++;
    }

    private void inc2() {
        synchronized (this) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count2++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo demo = new Demo();
        for (int i = 0; i < 1000; i++) {

            new Thread(() -> Demo.inc()).start();

            /*new Thread(() -> {
                demo.inc1();
            }).start();

            new Thread(() -> {
                demo.inc2();
            }).start();*/

            new Thread(() -> {
                Demo demo1 = new Demo();
                demo1.inc1();
            }).start();

            new Thread(() -> {
                Demo demo2 = new Demo();
                demo2.inc2();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println("運行結果:" + count);
        System.out.println("運行結果1:" + count1);
        System.out.println("運行結果2:" + count2);
    }
}

public class SynchronizedDemoTest {
    private static Object object = new Object();

    public static void main(String[] args) throws Exception {
        synchronized (object) {

        }
    }

    public static synchronized void method() {
    }
}

輸出結果如圖:


運行代碼結果圖

synchronized有三種方式來加鎖,分別是
1、修飾實例方法,作用于當前實例加鎖,進入同步代碼之前要獲得當前實例的鎖
2、修飾靜態方法,作用于當前類對象加鎖,進入同步代碼之前要獲得當前類對象的鎖
3、修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊之前要獲得給定對象的鎖

synchronized括號后面的對象
synchronized括號后面的對象是一把鎖,在java中任意一個對象都可以成為鎖,簡單來說,我們把object比喻成一個key,擁有這個key的線程才能執行這個方法,拿到這個key以后在執行方法過程中,這個key是隨身攜帶的,并且只有一把。如果后續的線程想訪問當前方法,因為沒有key所以不能訪問只能在門口等著,等之前的線程把key放回去。所以synchronized鎖定的對象必須是同一個,如果是不同對象,就意味著是不同的房間的要是,對于訪問者來說是沒有任何影響的。

synchronized的字節碼指令
通過javap -v SynchronizedDemoTest .class (會輸出行號、本地變量表信息、反編譯匯編代碼、輸出當前類用到的常量池等信息) 來查看對應的字節碼指令,對于同步塊的實現使用了monitorenter和monitorexit指令,這兩個指令隱式地執行了lock和unlock操作,用于提供原子性的保證。
monitorenter指令插入到同步代碼塊開始的位置,monitorexit指令插入到同步代碼塊結束的位置,jvm需要保證每個monitorenter都有一個monitorexit對應。
這兩個指令,本質上是對一個對象的監視器(monitor)進行獲取,這個過程是排他的,也就是說同一時刻只有一個線程獲得由synchronized所保護對象的監視器。
線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor所有權,也就是嘗試獲取對象的鎖,而執行monitorexit就是釋放monitor的所有權。
同步代碼塊使用了 monitorenter 和 monitorexit 指令實現。
同步方法中依靠方法修飾符上的 ACC_SYNCHRONIZED 實現。

public static void main(java.lang.String[]) throws java.lang.Exception;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field object:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter     // 監視器進入,獲取鎖
         6: aload_1
         7: monitorexit        //監視器退出,釋放鎖
         8: goto          16
        11: astore_2
        12: aload_1
        13: monitorexit //監視器退出,釋放鎖
        14: aload_2
        15: athrow
        16: return

 public static synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 13: 0

synchronized的鎖的原理
jdk1.6以后對synchronized鎖進行了優化,包含偏向鎖、輕量級鎖、重量級鎖,在了解synchronized之前,我們要了解兩個重要的概念,對象頭和monitor。

Java對象頭
在hotspot虛擬機中,對象在內存中的布局分為三塊區域:對象頭、實例數據和對齊填充,Java對象頭是實現synchronized的鎖對象的基礎,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里,它是輕量級鎖和偏向鎖的關鍵。

Mark Word
Mark Word是用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程id、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等于4字節,也就是32bit)


mark world

在源碼中的體現
如果想更深入了解對象頭在JVM源碼中的定義,需要關心幾個文件,oop.hpp/markOop.hpp
oop.hpp,每個Java Object在JVM內部都有一個native的C++對象 oop/oopDesc 與之對應,現在oop.hpp中看oopDesc的定義


src\share\vm\oops\oop.hpp

_mark被生命在oopDesc類的頂部,所以這個_mark可以認為是一個頭部,上面講過頭部保存了一些重要的狀態和標識信息,在markOop.hpp文件中有一些注釋說明markOop的內存布局,如圖


src\share\vm\oops\markOop.hpp

Monitor
monitor可以理解為一個同步工具,也可以描述為一種同步機制。所有的Java對象是天生的monitor,每個object對象里的markOop->monitor()可以保存ObjectMonitor的對象,從源碼層面分析monitor對象:
1、oop.hpp下的oopDesc類是JVM對象的頂級基類,所以每個object對象都包含markOop
2、markOop.hpp中markOopDesc繼承自oopDesc,并擴展了自己的monitor方法,這個方法返回一個ObjectMonitor指針對象
3、objectMonitor.hpp在hotspot虛擬機中,采用ObjectMonitor類來實現monitor,如圖


ObjectMonitor

synchronized的鎖升級和獲取過程
了解了對象以及monitor以后,接下來去分析synchronized的鎖的實現,就比較容易理解了。前面講過synchronized的鎖是進行過優化的,引入了偏向鎖、輕量級鎖,鎖的級別從低到高逐步升級,無鎖->偏向鎖->輕量級鎖->重量級鎖。

自旋鎖(CAS)
自旋鎖就是讓不滿足條件的線程等待一段時間,而不是立即掛起,看持有鎖的線程是否能夠很快釋放鎖,實現自旋的方式其實就是一段沒有任何意義的循環。
雖然它通過占用處理器的時間來避免線程切換帶來的開銷,但是如果持有鎖的線程不能在很快釋放鎖,那么自旋的線程就會浪費處理器的資源,因為它不會做任何有意義的工作。所以,自旋等待的時間或者次數是有一個限度的,如果自旋超過了定義的時間仍然沒有獲取到鎖,則該線程應該被掛起。

偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步快并獲取鎖的時候,會在對象頭和棧幀中的鎖記錄里面存儲偏向鎖的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試一下Mark Word中偏向鎖的表示是否設置成1 (表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

輕量級鎖
引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖。

重量級鎖
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴于底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。上面在講Java對象頭的時候,講到了monitor這個對象,在hotspot虛擬機中,通過ObjectMonitor類實現monitor,它的鎖的獲取過程的體現會簡單很多。


獲取鎖和獲取鎖失敗后的簡單流程圖

wait和notify
wait和notify是用來讓線程進入等待狀態以及使得線程喚醒的兩個操作

public class ThreadWait extends Thread {
    
    private final Object lock;

    ThreadWait(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("開始執行 thread wait");
            try {
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("執行結束 thread wait");
        }
    }
}

public class ThreadNotify extends Thread {
    
    private final Object lock;

    ThreadNotify(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println("開始執行 thread notify");
            lock.notify();
            System.out.println("執行結束 thread notify");
        }
    }
}

public class ThreadWaitNotifyDemo {

    public static void main(String[] args) {
        Object lock = new Object();
        ThreadWait threadWait = new ThreadWait(lock);
        threadWait.start();
        ThreadNotify threadNotify = new ThreadNotify(lock);
        threadNotify.start();

    }
}

輸出結果


結果圖

wait和notify的原理
調用wait方法,首先會獲取監視器鎖,獲得成功后,會讓當前線程進入等待隊列并且釋放鎖;然后當其他線程調用notify或者notifyAll以后,會選擇從等待隊列中喚醒任意一個線程,而執行完notify方法以后,并不會立馬喚醒線程,原因是當前的線程仍然持有這把鎖,處于等待狀態的線程無法獲得鎖,必須要等到當前的線程執行完monitorexit指令后,也就是鎖被釋放后,處于等待隊列中的線程才可以開始競爭鎖,如圖


線程獲得鎖和釋放鎖的過程

wait和notify為什么需要在synchronized里面?
wait方法的語義有兩個,一個是釋放當前的對象鎖、另一個是使得當前線程進入阻塞隊列,而這些操作都和監視器是相關的,所以wait必須要獲得一個監視器鎖;而對于notify來說也是一樣,它是喚醒一個線程,既然要去喚醒,首先得知道它在哪里,所以就必須要找到這個對象獲取到這個對象鎖,然后到這個對象的等待隊列中去喚醒一個線程。

學習來源https://www.gupaoedu.com/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容