Java併發-深刻理解synchronized

synchronized基礎用法

synchronized的三種應用方式:java

  1. 做用於實例方法,當前實例加鎖,進入同步代碼前要得到當前實例的鎖;
  2. 做用於靜態方法,當前類加鎖,進去同步代碼前要得到當前類對象的鎖;
  3. 做用於代碼塊,這須要指定加鎖的對象,對所給的指定對象加鎖,進入同步代碼前要得到指定對象的鎖。

用法總結以下:安全

clipboard.png

注:併發

  1. 不管synchronized關鍵字加在方法上仍是對象上,若是它做用的對象是非靜態的,則它取得的鎖是對象;若是synchronized做用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類全部的對象同一把鎖;
  2. 每一個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就能夠運行它所控制的那段代碼;
  3. 不管是方法正常執行完畢或者方法拋出異常,都會釋放鎖;
  4. synchronized不能夠被繼承,父類某個方法加了synchronized,若子類覆寫了該方法,子類要想同步還得在子類方法上加上synchronized關鍵字。

synchronized與wait、nofity、nofityAll配合使用

【問題】實現一個容器,提供兩個方法,add,size。寫兩個線程,線程1添加10個元素到容器中,線程2實現監控元素的個數,當個數到5個時,線程2給出提示並結束。
public class MyContainer {app

private static List<Integer> lists = new ArrayList<>();

public static void main(String[] args) {
    final Object lock = new Object();

    //監控線程
    new Thread(()->{
        synchronized (lock) {
            System.out.println("thread 2 start...");
            if(lists.size() != 5) {
                try {
                    lock.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println("thread 2 end.");
            lock.notify();
        }
    }, "t2").start();
    
    new Thread(()->{
        synchronized (lock) {
            for(int i = 0; i < 10; i++) {
                System.out.println("thread 1, add " + i);
                lists.add(i);

                if(lists.size() == 5) {
                    lock.notify();
                    try {
                        lock.wait();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }, "t1").start();
}

}性能

注:
wait()會馬上釋放synchronized(obj)中的obj鎖,以便其餘線程能夠執行obj.notify(),可是notify()不會馬上馬上釋放sycronized(obj)中的obj鎖,必需要等notify()所在線程執行完synchronized(obj)塊中的全部代碼纔會釋放這把鎖。因此,在t1中,調用了lock對象的notify方法以後,再調用lock的wait方法釋放鎖,而在t2被喚醒以後,繼續執行,最後還要調用lock對象的notify方法去喚醒此時處在wait狀態的t1優化

synchronized原理

原理概述

先經過下面簡單的例子看下:spa

public class Synchronize {操作系統

public static void main(String[] args) {
    synchronized (Synchronize.class){
        System.out.println("Synchronize");
    }
}

}.net

使用 javap -c Synchronize 能夠查看編譯以後的具體信息線程

clipboard.png

從編譯後的結果能夠看到:在同步方法調用前加了一個 monitorenter 指令,在退出方法和異常處插入了 monitorexit 的指令

實現原理:JVM 是經過進入、退出對象監視器( Monitor )來實現對方法、同步塊的同步的,具體實現是在編譯以後同步代碼塊採用添加moniterenter、moniterexit,同步方法使用ACC_SYNCHRONIZED標記符隱式實現。每一個對象都有一個monitor與之關聯,運行到moniterenter時嘗試獲取對應monitor的全部權,獲取成功就將monitor的進入數加1(因此是可重入鎖,也被稱爲重量級鎖),不然就阻塞,擁有monitor的線程運行到moniterexit時進入數減1,爲0時釋放monitor。其本質就是對一個對象監視器( Monitor )進行獲取,而這個獲取過程具備排他性從而達到了同一時刻只能一個線程訪問的目的。Java內置的synchronized關鍵字能夠認爲是管程模型中的MESA模型的簡化版。

Java對象如何與Monitor關聯

Java對象與Monitor關聯關係示意圖以下:

圖片描述

JVM堆中存放的是對象實例,每個對象都有對象頭,對象頭裏有Mark Word,裏面存儲着對象的hashCode、GC分代年齡以及鎖信息。如圖所示,重量級鎖中存有指向monitor的指針。
其中ObjectMonitor中幾個關鍵字段的含義以下:
_count:記錄owner線程獲取鎖的次數。這句話很好理解,這也決定了synchronized是可重入的。
_owner:指向擁有該對象的線程
_WaitSet:主要存放全部wait的線程的對象,也就是說若是有線程處於wait狀態,將被掛入這個隊列,調用了wait()方法線程會進入該隊列
_EntryList:全部在等待獲取鎖的線程的對象,也就是說若是有線程處於等待獲取鎖的狀態的時候,將被掛入這個隊列。

圖片描述

詳情請參考:https://www.jianshu.com/p/32e...

Monitor加鎖及解鎖過程

圖片描述

  1. 當多個線程同時訪問一段同步代碼時,首先會進入_EntryList隊列中,當某個線程獲取到對象的monitor後進入_Owner區域並把monitor中的_owner變量設置爲當前線程,同時monitor中的計數器_count加1。即得到對象鎖;
  2. 若持有monitor的線程調用wait()方法,將釋放當前持有的monitor,_owner變量恢復爲null,_count自減1,同時該線程進入_WaitSet集合中等待被喚醒;
  3. 當程序裏其餘線程調用了notify/notifyAll方法時,就會喚醒_waitSet中的某個線程,這個線程就會再次嘗試獲取monitor鎖。若是成功,則就會成爲monitor的owner;
  4. 若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取。

詳細過程請參考:https://www.hollischuang.com/...

加鎖和解鎖的內存語義

Java內存模型:

clipboard.png

Java內存模型雖然有助加快執行速度,可是也帶來了新的問題。不一樣線程之間是沒法之間訪問對方工做內存中的變量,線程間的變量值傳遞均須要經過主內存來完成,那線程的操做結果怎麼讓其餘線程可見呢?這便須要先行發生(happens-before)原則來保證了。

先行發生規則中有以下2條:

  1. 對同一個監視器的解鎖,happens-before於對該監視器的加鎖
  2. 若是A happens-before B,則A的執行結果對B可見,而且A的執行順序先於B

即:若是有2個線程A和B,則根據規則1,A線程釋放鎖 happens-before B線程獲取鎖,根據規則2,那A線程的操做結果對B線程是可見的。

clipboard.png

clipboard.png

從上圖能夠看出,線程A會首先先從主內存中讀取共享變量a=0的值而後將該變量拷貝到本身的本地內存,進行加1操做後,再將該值刷新到主內存,整個過程即爲線程A 加鎖-->執行臨界區代碼-->釋放鎖相對應的內存語義。線程B獲取鎖的時候一樣會從主內存中讀取共享變量a的值,這個時候就是最新的值1,而後將該值拷貝到線程B的工做內存中去,釋放鎖的時候一樣會重寫到主內存中。
即:釋放鎖的時候會將值刷新到主內存中,其餘線程獲取鎖時會強制從主內存中獲取最新的值。這也驗證了A happens-before B,A的執行結果對B是可見的。

詳情請參考:https://www.jianshu.com/p/151...

鎖的優化

高效併發是從JDK 1.5 到 JDK 1.6的一個重要改進,HotSpot虛擬機開發團隊在這個版本中花費了很大的精力去對Java中的鎖進行優化,如適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等。這些技術都是爲了在線程之間更高效的共享數據,以及解決競爭問題。但對Java開發者而言,只須要知道想在加鎖的時候使用synchronized就能夠了,具體的鎖的優化是虛擬機根據競爭狀況自行決定的
因爲Java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統的幫忙,這就要從用戶態轉換到內核態,所以狀態轉換須要花費不少的處理器時間,因此優化的想法主要是能不阻塞線程就不阻塞。

  1. 適應性自旋:所謂的自旋鎖就是讓線程不停地執行循環體,不進行線程狀態的改變。若是在鎖被佔用的時間很短的狀況下,自旋等待的效果會很是好,反之,若是鎖被佔用的時間很長,自旋就會浪費CPU,因此自旋要有必定限度。在JDK1.6後,自旋的時間再也不固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定,這即是自適應自旋了。
  2. 鎖消除:經過逃逸分析,判斷出代碼塊中不存在多個線程共享的數據,便會在編譯後將鎖去掉。好比:咱們常常在代碼中使用StringBuffer做爲局部變量,而StringBuffer中的append是線程安全的,有synchronized修飾的,可是做爲局部變量並不須要共享,因此這個時候便會進行鎖消除的優化。
  3. 鎖粗化:須要加鎖的時候,咱們提倡儘可能減少鎖的粒度,這樣能夠避免沒必要要的阻塞。可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那便是沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。因此當虛擬機探測到這樣的狀況時,就會把加鎖的範圍擴大
    如如下代碼:

    clipboard.png
    會被粗化成:

    clipboard.png

  4. 輕量級鎖:其實就是指經過CAS操做嘗試把monitor的_owner字段設置爲當前線程,若是更新成功了,那麼代表這個線程就擁有了該對象的鎖,並將對象頭的Mark Word的鎖標誌位轉變爲"00",即表明此對象處於輕量級鎖定狀態。若是更新失敗,則膨脹爲重量級鎖,等待鎖的線程須要進入阻塞狀態。經過ObjectMonitor類的源碼能夠看出:

    clipboard.png

  5. 偏向鎖:意思是這個鎖會偏向於第一個得到它的線程,若是在接下來執行過程當中,該鎖沒有被其餘線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。若是說輕量級鎖是在無競爭的狀況下使用CAS操做去消除同步使用的互斥量,那麼偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS操做都不作了。但這一切都是在無競爭的狀況下,若是有另一個線程嘗試去獲取這個鎖,那偏向模式便宣告結束。

細節請參考:https://www.hollischuang.com/...

總結

本文從synchronized的用法開始,而後逐步深刻介紹synchronized的實現原理,其實質是對管程模型的一種實現。雖然在用的時候就是一個關鍵字,但背後的內容卻十分豐富,寫本文的過程當中,參考了許多大牛的博客,受益良多。

參考

https://blog.csdn.net/weixin_...
https://www.jianshu.com/p/32e...
https://www.jianshu.com/p/d53...
https://www.hollischuang.com/...
https://www.hollischuang.com/...
https://www.hollischuang.com/...
https://www.jianshu.com/p/e62...
https://www.jianshu.com/p/27f...
https://www.jianshu.com/p/151...

相關文章
相關標籤/搜索