【原創】Java併發編程系列07 | synchronized原理

【原創】Java併發編程系列07 | synchronized原理

收錄於話題
#進階架構師 | 併發編程專題
12個

點擊上方「java進階架構師」,選擇右上角「置頂公衆號」
20大進階架構專題每日送達
【原創】Java併發編程系列07 | synchronized原理
併發編程中用到最多的關鍵字毫無疑問是synchronized。這篇文章就來探究下synchronized:
synchronized如何使用?
synchronized是實現同步加鎖的原理?
synchronized解決了併發編程的哪些問題?
【原創】Java併發編程系列07 | synchronized原理html

1. synchronized使用

1.1 線程安全問題
併發編程中,當多個線程同時訪問同一個資源的時候,就會存在線程安全問題。
因爲每一個線程執行的過程是不可控的,因此極可能致使最終的結果與實際指望的結果相違背或者直接致使程序出錯。
舉例:java

public classVolatileTest {
    public int inc = 0;

    public void increase() {
       inc++;
    }

    public static void main(String[] args) {
       final VolatileTest test = newVolatileTest();
       for (int i = 0; i < 10; i++) {
           new Thread() {
              public void run() {
                  for (int j = 0; j < 1000;j++)
                     test.increase();
              };
           }.start();
       }

       while (Thread.activeCount() > 1)
           // 保證前面的線程都執行完
           Thread.yield();
       System.out.println(test.inc);
    }
}

目的:test.inc = 10000
結果:屢次執行獲得的結果都小於10000
分析:線程安全問題。
當某個時間test.inc=2,有多個線程同時讀取到test.inc=2,而且同時執行加1操做,這些線程的這次操做都執行以後test.inc=3。也就是說執行了多個加1操做,卻只將結果增長了1,因此致使最終結果始終小於10000。c++

基本上全部的併發模式在解決線程安全問題時,都採用「序列化訪問臨界資源」的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱做同步互斥訪問。
一般來講,是在訪問臨界資源的代碼前面加上一個鎖,當訪問完臨界資源後釋放鎖,讓其餘線程繼續訪問。
在Java中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock。面試

Java中用synchronized標記同步塊。編程

  • 同步塊在Java中是同步在某個對象上(監視器對象)。
  • 全部同步在一個對象上的同步塊在同一時間只能被一個線程進入並執行操做。
  • 全部其餘等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。
    1.2 synchronized用法安全

  • 普通同步方法,鎖是當前實例對象
  • 靜態同步方法,鎖是當前類的class對象
  • 同步方法塊,鎖是括號裏面的對象
    舉例:
public class MyClass{
    int count;

    // 1.實例方法
    public synchronized void add(int value){
        count += value;
    }

    // 2.實例方法中的同步塊 (等價於1)
    public void add(int value){
        synchronized(this){
            count += value;
        }
    }

    // 3.靜態方法
    public static synchronized void add(intvalue){
         count += value;
    }

    // 4.靜態方法中的同步塊 (等價於3)
    public static void add(int value){
        synchronized(MyClass.class){
            count += value;
        }
    }
}

2. 原理探究


以下代碼,利用javap工具查看生成的class文件信息來分析Synchronize的實現。
代碼:markdown

public class synchronized Test {
    // 同步代碼塊
    public void doSth1(){
       synchronized (synchronizedTest.class){
           System.out.println("HelloWorld");
       }
    }
    // 同步方法
    public synchronized void doSth2(){
        System.out.println("HelloWorld");
    }
}

使用javap對class文件進行反編譯後結果:
javap命令:
D:\install\java\jdk8\bin\javap.exe -v .\synchronizedTest.class架構

同步代碼塊併發

同步方法
從反編譯後的結果中能夠看到:對於同步方法,JVM採用ACC_synchronized標記符來實現同步。對於同步代碼塊。JVM採用monitorenter、monitorexit兩個指令來實現同步。
同步代碼塊oracle

JVM採用monitorenter、monitorexit兩個指令來實現同步。
查詢JVM規範The Java® Virtual Machine Specification[1]中關於monitorenter和monitorexit的介紹:
【原創】Java併發編程系列07 | synchronized原理
大體內容以下:
能夠把執行monitorenter指令理解爲加鎖,執行monitorexit理解爲釋放鎖。
每一個對象維護着一個記錄着被鎖次數的計數器。
未被鎖定的對象的該計數器爲0,當一個線程得到鎖(執行monitorenter)後,該計數器自增變爲1,當同一個線程再次得到該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行monitorexit指令)的時候,計數器再自減。
當計數器爲0的時候。鎖將被釋放,其餘線程即可以得到鎖。
同步方法

JVM採用ACC_synchronized標記符來實現同步。
查詢JVM規範The Java® Virtual Machine Specification[2]中關於方法級同步的介紹:
【原創】Java併發編程系列07 | synchronized原理
大體內容以下:
方法級的同步是隱式的。同步方法的常量池中會有一個ACC_synchronized標誌。
當某個線程要訪問某個方法的時候,會檢查是否有ACC_synchronized,若是有設置,則須要先得到監視器鎖(monitor),而後開始執行方法,方法執行以後再釋放監視器鎖。這時若是其餘線程來請求執行方法,會由於沒法得到監視器鎖而被阻斷住。
值得注意的是,若是在方法執行過程當中,發生了異常,而且方法內部並無處理該異常,那麼在異常被拋到方法外面以前監視器鎖會被自動釋放。

3. Monitor


不管是同步方法仍是同步代碼塊都是基於監視器Monitor實現的。
Monitor是什麼?

全部的Java對象是天生的Monitor,每個Java對象都有成爲Monitor的潛質,由於在Java的設計中,每個Java對象自打孃胎裏出來就帶了一把看不見的鎖,它叫作內部鎖或者Monitor鎖。
每一個對象都存在着一個Monitor與之關聯,對象與其Monitor之間的關係有存在多種實現方式,如Monitor能夠與對象一塊兒建立銷燬。
Moniter如何實現線程的同步?

在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的(位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的)。
ObjectMonitor中有幾個關鍵屬性:
_owner:指向持有ObjectMonitor對象的線程
_WaitSet:存放處於wait狀態的線程隊列
_EntryList:存放處於等待鎖block狀態的線程隊列
_recursions:鎖的重入次數
_count:用來記錄該線程獲取鎖的次數

  • 線程T等待對象鎖:_EntryList中加入T。
  • 線程T獲取對象鎖:_EntryList移除T,_owner置爲T,計數器_count加1。
  • 線程T中鎖對象調用wait():_owner置爲null,計數器_count減1,_WaitSet中加入T等待被喚醒。
  • 持有對象鎖的線程T執行完畢:復位變量的值,以便其餘線程進入獲取monitor。
    【原創】Java併發編程系列07 | synchronized原理

    4. 解決三大問題


保證原子性

在併發編程中的原子性:一段代碼,或者一個變量的操做,在一個線程沒有執行完以前,不能被其餘線程執行。
synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放以前,沒法被其餘線程訪問到。

即便在執行過程當中,CPU時間片用完,線程放棄了CPU,但並無進行解鎖。而因爲synchronized的鎖是可重入的,下一個時間片仍是隻能被他本身獲取到,仍是會由同一個線程繼續執行代碼,直到全部代碼執行完。從而保證synchronized修飾的代碼塊在同一時間只能被一個線程訪問。

保證有序性

若是在本線程內觀察,全部操做都是自然有序的。
——《深刻理解Java虛擬機》

單線程重排序要遵照as-if-serial語義,無論怎麼重排序,單線程程序的執行結果都不能被改變。由於不會改變執行結果,因此無須關心這種重排的干擾,能夠認爲單線程程序是按照順序執行的。
synchronized修飾的代碼,同一時間只能被同一線程訪問。那麼也就是單線程執行的。因此,能夠保證其有序性。
保證可見性

加鎖的含義不只僅侷限於互斥行爲,還包括可見性。
——《Java併發編程實戰》

JMM關於synchronized的兩條語義規定保證了可見性:

  • 線程解鎖前,必須把共享變量的最新值刷新到主內存中。
  • 線程加鎖前,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新的值。

    5. 總結


多併發編程中經過同步互斥訪問臨界資源來解決線程安全問題,Java中經常使用synchronized標記同步塊達到加鎖的目的。
synchronized用法有兩種,修飾方法和修飾同步代碼塊。
synchronized的實現原理:每個Java對象都會關聯一個Monitor,經過Monitor對線程的操做實現synchronized對象鎖。
併發編程中synchronized能夠保證原子性、可見性、有序性。
參考資料

[1]
The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter
[2]
The Java® Virtual Machine Specification: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

併發系列文章彙總(點擊標題可跳轉)


【原創】Java併發編程系列01 | 開篇獲獎感言
【原創】Java併發編程系列02 | 併發編程三大核心問題
【原創】Java併發編程系列03 | 重排序-可見性和有序性問題根源
【原創】Java併發編程系列04 | Java內存模型詳解
【原創】Java併發編程系列05 | 深刻理解volatile
【原創】Java併發編程系列06 | 你不知道的final
———— e n d ————
微服務、高併發、JVM調優、面試專欄等20大進階架構師專題請關注公衆號【Java進階架構師】後在菜單欄查看。
回覆【架構】領取架構師視頻一套。
【原創】Java併發編程系列07 | synchronized原理原創歷來不開讚揚是由於我以爲你的「在看」,就是給我最好的讚揚^_^

相關文章
相關標籤/搜索