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

synchronized 具備使每一個線程依次排隊操做共享變量的功能。這種同步機制效率很低,但 synchronized 是其它併發容器實現的基礎。html

 

1、鎖對象及 synchronized 的使用

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

鎖對象分爲兩種:數組

實例對象(一個類有多個)和 Class 對象(一個類只有一個)。數據結構

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

使用 synchronized 也有兩種方式:oracle

修飾普通方法,鎖當前實例對象。修飾靜態方法,鎖當前類的 Class 對象。jvm

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

class Xz {
    // 類鎖
    public static synchronized void aa() {
        for (int i = 0; i < 10; i++) {
            System.out.println("aaa");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // 對象鎖
    public synchronized void bb() {
        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();
            }
        }
    }
}

public class SynchronizedTest {
    public static void main(String[] args) {
        Xz xz = new Xz();
        // 執行互不干擾
        new Thread(() -> {
            Xz.aa();
        }).start();
        new Thread(() -> {
            xz.bb();
        }).start();
        new Thread(() -> {
            xz.cc();
        }).start();
    }
}
View Code

 

2、特性

原子性

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

可見性

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

有序性

synchronized 自己是沒法禁止指令重排和處理器優化的。

as-if-serial 語義:無論怎麼重排序(編譯器和處理器爲了提升並行度),單線程程序的執行結果都不能被改變。

編譯器和處理器不管如何優化,都必須遵照 as-if-serial 語義。

synchronized 修飾的代碼,同一時間只能被同一線程執行。因此,能夠保證其有序性。

 

3、synchronized 的實現:monitor 和 ACC_SYNCHRONIZED

package com;

/**
 * 編譯:javac com\SynchronizedTest.java
 * 反編譯:javap -v com\SynchronizedTest
 */
public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("haha!");
        }
    }

    public synchronized void xx(){
        System.out.println("xixi!");
    }
}
View Code

反編譯上述代碼,結果以下(省去了不相關信息)

{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter // 獲取鎖,以後其它要執行該段代碼的線程須要等鎖釋放
         5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #4                  // String haha!
        10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit // 鎖內代碼執行完畢,釋放鎖,其餘線程可再次獲取鎖
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit // 鎖內代碼發生異常時自動釋放鎖
        21: aload_2
        22: athrow
        23: return

  public synchronized void xx();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 線程在執行有 ACC_SYNCHRONIZED 標誌的方法時須要先得到鎖
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String xixi!
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 15: 0
        line 16: 8
}

同步代碼塊

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

使用 monitorentermonitorexit 兩個指令實現。

每一個對象維護着一個記錄着被鎖次數的計數器。未被鎖定的對象的該計數器爲 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 實現的。

 

4、Mark Word 與 ObjectMonitor

對象的實例保存在堆上,對象的元數據保存在方法區,對象的引用保存在棧上。

對象的實例在堆中的數據可分爲對象頭(包含 Mark Word 和 Class Metadata Address),實例數據,對齊填充(HotSpot 要求對象的起止地址必須是 8 的倍數)。

對象頭在 JVM 中對應的對象文件爲 markOop.hpp,其中引用了 ObjectMonitor 對象文件。

Mark Word

對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,對象頭被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間。

下圖描述了在 32 位虛擬機上,非數組對象在不一樣狀態時 mark word 各個比特位區間的含義。若是是數組對象的話,還會有一個額外的部分用於存儲數組長度。 

源碼中(markOop.hpp)關於對象頭對象的定義,主要包含了 GC 分代年齡、鎖狀態標記、哈希碼、epoch(偏向時間戳)等信息。

enum {  age_bits                 = 4,
        lock_bits                = 2,
        biased_lock_bits         = 1,
        max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
        hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits,
        cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
        epoch_bits               = 2
};

源碼中(markOop.hpp)關於對象頭中鎖狀態的定義。

enum {  locked_value             = 0, // 00  輕量級鎖
        unlocked_value           = 1, // 001 無鎖
        monitor_value            = 2, // 10  監視器鎖,膨脹鎖,重量級鎖
        marked_value             = 3, // 11  GC標記
        biased_lock_pattern      = 5  // 101 偏向鎖
};

ObjectMonitor

源碼中(objectMonitor.hpp)關於 Monitor 對象的定義。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 用來記錄該線程獲取鎖的次數
    _waiters      = 0,
    _recursions   = 0; // 鎖的重入次數
    _object       = NULL;
    _owner        = NULL; // 指向持有 ObjectMonitor 對象的線程
    _WaitSet      = NULL; // 存放處於 wait 狀態的線程隊列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 存放處於等待鎖 block 狀態的線程隊列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}

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

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

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


https://www.hollischuang.com/archives/2637

https://www.hollischuang.com/archives/tag/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E5%A4%9A%E7%BA%BF%E7%A8%8B

http://www.javashuo.com/article/p-kigpdlhx-a.html

http://www.javashuo.com/article/p-qwevqyhu-dg.html

http://www.javashuo.com/article/p-udhwoaww-co.html

相關文章
相關標籤/搜索