Java-JUC(十五):synchronized執行流程分析

1、鎖對象及 synchronized 的使用

synchronized 經過互斥鎖(Mutex Lock)來實現,同一時刻,只有得到鎖的線程才能夠執行鎖內的代碼。html

鎖對象分爲兩種:java

實例對象(一個類有多個)和 Class 對象(一個類只有一個)。git

不一樣鎖對象之間的代碼執行互不干擾,同一個類中加鎖方法與不加鎖方法執行互不干擾。github

使用 synchronized 有如下種方式:segmentfault

修飾普通方法,鎖當前實例對象。數組

修飾靜態方法,鎖當前類的 Class 對象。併發

修飾代碼塊,鎖括號中的對象(實例對象或 Class 對象)。oracle

示例:app

class SynchronizedDemo {
    // 類鎖(修飾靜態方法:鎖當前類的 Class 對象。)
    public static synchronized void inStaticMethod() {
        for (int i = 0; i < 10; i++) {
            System.out.println("aaa");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }    
    // 類鎖(修飾代碼塊,鎖括號中的 Class 對象)
    public static void inStaticMethodLockClassObj() {
        synchronized(SynchronizedDemo.class){
            for (int i = 0; i < 10; i++) {
                System.out.println("aaa");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 對象鎖(修飾普通方法:鎖當前實例對象)
    public synchronized void inNormalMethod() {
        for (int i = 0; i < 10; i++) {
            System.out.println("bbb");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    } 
    // 對象鎖(修飾代碼塊:鎖括號中的實例對象)
    public  void bb() {
        synchronized(this){
            for (int i = 0; i < 10; i++) {
                System.out.println("bbb");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 無鎖
    public void cc() {
        for (int i = 0; i < 10; i++) {
            System.out.println("ccc");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
View Code

2、特性

原子性異步

被 synchronized 修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放以前,沒法被其餘線程訪問到。所以,在 Java 中可使用 synchronized 來保證方法和代碼塊內的操做是原子性的。

可見性

對一個變量解鎖以前,必須先把此變量同步回主存中。這樣解鎖後,後續線程就能夠訪問到被修改後的值。

有序性

synchronized 自己是沒法禁止指令重排和處理器優化的,
as-if-serial 語義:無論怎麼重排序(編譯器和處理器爲了提升並行度),單線程程序的執行結果都不能被改變。
編譯器和處理器不管如何優化,都必須遵照 as-if-serial 語義。
synchronized 修飾的代碼,同一時間只能被同一線程執行。因此,能夠保證其有序性。

3、靜態方法內部的代碼塊執行分析

測試代碼:

public class DriverInstance {
    private static DriverInstance instance = null;

    private DriverInstance() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static DriverInstance getInstance() {
        if (instance == null) {
            synchronized (DriverInstance.class) {
                System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->locking ");
                if (instance == null) {
                    System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->begin set value ");
                    instance = new DriverInstance();
                    System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->end set value ");
                }
                System.out.println(System.nanoTime()+"-> "+Thread.currentThread().getName()+" ->unlock ");
            }
        }

        return instance;
    }
}

public class Main {
    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(DriverInstance.getInstance());
                    countDownLatch.countDown();
                }
            }).start();
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("first complete...");

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(DriverInstance.getInstance());
                }
            }).start();
        }
    }
}

打印結果:

1027791747517115-> Thread-0 ->locking 
1027791747791125-> Thread-0 ->begin set value 
1027792830581000-> Thread-0 ->end set value 
1027792830905707-> Thread-0 ->unlock 
1027792831314389-> Thread-9 ->locking 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792831593375-> Thread-9 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792831900975-> Thread-8 ->locking 
1027792832238745-> Thread-8 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792832385236-> Thread-6 ->locking 
1027792832555676-> Thread-6 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792832681639-> Thread-7 ->locking 
1027792832829374-> Thread-7 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792832990484-> Thread-4 ->locking 
1027792833090010-> Thread-4 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833279111-> Thread-5 ->locking 
1027792833473500-> Thread-5 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833549389-> Thread-3 ->locking 
1027792833643007-> Thread-3 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833709254-> Thread-2 ->locking 
1027792833797895-> Thread-2 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
1027792833875651-> Thread-1 ->locking 
1027792833993217-> Thread-1 ->unlock 
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
first complete...
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e
com.boco.icos.mrfingerlib.common.cfgs.MRSCfgExpressionParser@32f647e

測試結果解讀:

1)多個線程是同時進入getInstance方法執行,在執行getInstance方法時(除了synchronized代碼塊之外的代碼)線程之間是異步執行的;

2)從上邊測試結果能夠看出,線程Thread-0優先進入了synchronized中(優先獲取到了synchronized鎖),這時其餘9個線程都在排隊等待進入synchronized中;

3)當thread-0釋放鎖後,才其餘線程排隊依次執行「獲取鎖、初始化 instance、釋放鎖」。

4)由於是第一次執行,多個線程中 instance 初始值都是null,所以當它們進入到第一個if(instance ==null),而後排隊獲取鎖,上邊測試結果中thread-0獲取到鎖後給 instance 賦值,賦值以後釋放鎖,釋放鎖的同時更新 instance 變量內存值(同時把全部的thread的本地副本變量刷新),當thread-0已經釋放了鎖後,隊列中的等待獲取鎖的其餘線程依次「獲取到鎖,進入鎖內部代碼執行第二個if(instance ==null)發現變量 instance 值已經不爲空,不執行 instance 賦值操做,釋放鎖」。

5)通過CountDownLatch的wait以後,從新啓動的10個線程,此時這10個線程在初始化的時候 instance 的內存值不爲空,每一個線程賦值 instance 到本地線程,而後執行getInstalce,進度第一個if(instance==null)判斷,發現不爲空,直接返回 instance 變量,根本不會進入到鎖內部。

4、synchronized 的實現:monitor 和 ACC_SYNCHRONIZED

synchronized的實現是基於monitor實現的,可是使用方式不一樣(修飾方法、修飾代碼塊),其內部實現有差異:

同步代碼塊

JVM 規範描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-3.html#jvms-3.14 

使用 monitorenter 和 monitorexit 兩個指令實現。

每一個對象維護着一個記錄着被鎖次數的計數器。未被鎖定的對象的該計數器爲 0。

當一個線程得到鎖(執行 monitorenter)後,該計數器自增變爲 1 ,當同一個線程再次得到該對象的鎖的時候,計數器再次自增(可重入性)。當同一個線程釋放鎖(執行 monitorexit)後,該計數器自減。當計數器爲0的時候,鎖將被釋放。

同步方法

JVM 規範描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

同步方法的常量池中會有一個 ACC_SYNCHRONIZED 標誌。當線程訪問時候,會檢查是否有 ACC_SYNCHRONIZED,有則須要先得到鎖,而後才能執行方法,執行完或執行發生異常都會自動釋放鎖。

ACC_SYNCHRONIZED 也是基於 Monitor 實現的。

舉例分析

來查看下具體的靜態方法內部的靜態代碼塊的一個示例:

編寫一個測試類DriverInstance.java:

package com.dx.test;

public class DriverInstance {
    private static DriverInstance instance = null;

    private DriverInstance() {
    }
    
    public static DriverInstance getInstance() {
        if (instance == null) {
            synchronized (DriverInstance.class) {
                if (instance == null) {
                    instance = new DriverInstance();
                }
            }
        }

        return instance;
    }
    
    public void test(){
        
    }
}

使用javac編譯DriverInstance.java代碼:

E:\work>javac DriverInstance.java

生成DriverInstance.class

使用javap -v DriverInstance.class反編譯DriverInstance.class

E:\work>javap -v DriverInstance.class
Classfile /E:/work/DriverInstance.class
  Last modified 2019-8-28; size 610 bytes
  MD5 checksum 73f4e1682a85c9a8cf854edfdd09261b
  Compiled from "DriverInstance.java"
public class com.dx.test.DriverInstance
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#22         // com/dx/test/DriverInstance.instance:Lcom/dx/test/DriverInstance;
   #3 = Class              #23            // com/dx/test/DriverInstance
   #4 = Methodref          #3.#21         // com/dx/test/DriverInstance."<init>":()V
   #5 = Class              #24            // java/lang/Object
   #6 = Utf8               instance
   #7 = Utf8               Lcom/dx/test/DriverInstance;
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               getInstance
  #13 = Utf8               ()Lcom/dx/test/DriverInstance;
  #14 = Utf8               StackMapTable
  #15 = Class              #24            // java/lang/Object
  #16 = Class              #25            // java/lang/Throwable
  #17 = Utf8               test
  #18 = Utf8               <clinit>
  #19 = Utf8               SourceFile
  #20 = Utf8               DriverInstance.java
  #21 = NameAndType        #8:#9          // "<init>":()V
  #22 = NameAndType        #6:#7          // instance:Lcom/dx/test/DriverInstance;
  #23 = Utf8               com/dx/test/DriverInstance
  #24 = Utf8               java/lang/Object
  #25 = Utf8               java/lang/Throwable
{
  public static com.dx.test.DriverInstance getInstance();
    descriptor: ()Lcom/dx/test/DriverInstance;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
         3: ifnonnull     37
         6: ldc           #3                  // class com/dx/test/DriverInstance
         8: dup
         9: astore_0
        10: monitorenter
        11: getstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
        14: ifnonnull     27
        17: new           #3                  // class com/dx/test/DriverInstance
        20: dup
        21: invokespecial #4                  // Method "<init>":()V
        24: putstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
        27: aload_0
        28: monitorexit
        29: goto          37
        32: astore_1
        33: aload_0
        34: monitorexit
        35: aload_1
        36: athrow
        37: getstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
        40: areturn
      Exception table:
         from    to  target type
            11    29    32   any
            32    35    32   any
      LineNumberTable:
        line 10: 0
        line 11: 6
        line 12: 11
        line 13: 17
        line 15: 27
        line 18: 37
      StackMapTable: number_of_entries = 3
        frame_type = 252 /* append */
          offset_delta = 27
          locals = [ class java/lang/Object ]
        frame_type = 68 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 23: 0

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: aconst_null
         1: putstatic     #2                  // Field instance:Lcom/dx/test/DriverInstance;
         4: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "DriverInstance.java"

備註:

aload 指令的解釋:從局部變量表的相應位置裝載一個對象引用到操做數棧的棧頂

aload_0把this裝載到了操做數棧中aload_0是一組格式爲aload_的操做碼中的一個,這一組操做碼把對象的引用裝載到操做數棧中標誌了待處理的局部變量表中的位置,但取值僅可爲0、一、2或者3。

還有一些其餘類似的操做碼用來裝載非對象引用,包括iload_、lload_、fload_和dload_,這裏的i表明int型,l表明long型,f表明float型以及d表明double型。在局部變量表中的索引位置大於3的變量的裝載可使用iload、lload、fload,、dload和aload,這些操做碼都須要一個操做數的參數,用於確認須要裝載的局部變量的位置。

astore 指令的解釋:將棧頂數值(objectref)存入當前frame的局部變量數組中指定下標(index)處的變量中,棧頂數值出棧。

在monitorexit以前都會調用aload操做,實際上咱們能夠理解爲「這裏就是實現內存可見性實現的,在釋放鎖以前把變量同步回主存中」。

5、鎖獲取和鎖釋放的內存語義

線程A.B同時開始執行,獲取主存中的x變量,x的變量初始值是0,線程A優先拿到鎖,此時線程A在「同步代碼塊」或者「同步方法」內修改了x變量的值爲1,當線程A釋放鎖以前會將修改x變量值刷新到主存中。
整個過程即爲線程A 加鎖-->執行臨界區代碼-->釋放鎖相對應的內存語義。

線程B獲取鎖的時候一樣會從主內存中共享變量x的值,這個時候就是最新的值1,而後將該值拷貝到線程B的工做內存中去,釋放鎖的時候一樣會重寫到主內存中。

從總體上來看,線程A的執行結果(a=1)對線程B是可見的,實現原理爲:釋放鎖的時候會將值刷新到主內存中,其餘線程獲取鎖時會強制從主內存中獲取最新的值。

從橫向來看,這就像線程A經過主內存中的共享變量和線程B進行通訊,A 告訴 B 咱們倆的共享數據如今爲1啦,這種線程間的通訊機制正好吻合java的內存模型正好是共享內存的併發模型結構。

6、Java對象如何與Monitor關聯

JVM堆中存放的是對象實例,每個對象都有對象頭,對象頭裏有Mark Word,裏面存儲着對象的hashCode、GC分代年齡以及鎖信息。

如圖所示,重量級鎖中存有指向monitor的指針。

32位的HotSpot虛擬機對象頭存儲結構:

爲了證明上圖的正確性,這裏咱們看openJDK--》hotspot源碼markOop.hpp(新的定義類:MarkWord,hpp),虛擬機對象頭存儲結構:

https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/oops/markWord.hpp

 

上圖中有源碼中對鎖標誌位這樣枚舉

openJDK中ObjectMonitor類定義:

https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/objectMonitor.hpp

 

ObjectMonitor中幾個關鍵字段的含義:
_recursions:鎖的重入次數。這句話很好理解,這也決定了synchronized是可重入的。
_owner:指向擁有該對象的線程
_WaitSet:主要存放全部wait的線程的對象,也就是說若是有線程處於wait狀態,將被掛入這個隊列,調用了wait()方法線程會進入該隊列。
_EntryList:全部在等待獲取鎖的線程的對象,也就是說若是有線程處於等待獲取鎖的狀態的時候,將被掛入這個隊列。

對象,對象監視器,同步隊列和線程狀態的關係:

圖中描述內容解釋:

在Synchronized使用中,任意線程對Object的訪問,首先要得到Object的監視器;
若是獲取失敗,該線程就進入同步狀態,線程狀態變爲BLOCKED;
當Object的監視器佔有者釋放後,在同步隊列中得線程就會有機會從新獲取該監視器。

實際上若是synchronized內部使用wait還會存在另一種wait狀態。

示例:

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 {
                  System.out.println("thread 2 start wait");
                     lock.wait();
                     System.out.println("thread 2 end wait");
                 } catch (Exception e) { e.printStackTrace(); }
             }
             System.out.println("thread 2 start notify");
             lock.notify();
             System.out.println("thread 2 end 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) {
                  System.out.println("thread 1 start notify");
                     lock.notify();
                  System.out.println("thread 1 end notify");
                     try {
                   System.out.println("thread 1 start wait");
                         lock.wait();
                      System.out.println("thread 1 end wait");
                     } catch (Exception e) { e.printStackTrace(); }
                 }
                 try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); }
             }
         }
     }, "t1").start();
  
 }

當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 隊列中,當某個線程獲取到對象的 monitor 後進入 _Owner 區域並把 monitor 中的 _owner 變量設置爲當前線程,同時 monitor 中的計數器 _count 加 1。即得到對象鎖。

若持有 monitor 的線程調用 wait() 方法,將釋放當前持有的 monitor,_owner 變量恢復爲 null,_count 自減 1,同時該線程進入 _WaitSet 集合中等待被喚醒。

若當前線程執行完畢也將釋放 monitor(鎖) 並復位變量的值,以便其餘線程進入獲取 monitor(鎖)。

示意圖以下:

 

參考:

jdk源碼剖析二: 對象內存佈局、synchronized終極原理

ava併發-深刻理解synchronized

Java-內存模型 synchronized 的內存語義

讓你完全理解Synchronized

相關文章
相關標籤/搜索