sychronized關鍵字學習筆記

1 併發編程中的三個問題

1.1 可見性

多個線程同時操作共享變量,當一個線程對該變量做出修改,其它線程立刻知曉

可見性問題演示:

/**
 * 演示可見性問題
 */
public class VisibilityDemo {

    // 共享變量
    private static boolean flag = true;
//  private volatile static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {

        // 1個線程不斷讀取共享變量
        new Thread(() -> {
            while (flag) {}
            System.err.println("while ended");
        }).start();

        Thread.sleep(2_000);

        // 1個線程修改共享變量
        new Thread(() -> flag = false).start();

    }

}

上述代碼中,flag沒有volatile修飾時,儘管第二個線程對flag做出修改,但第一個線程的while循環仍然不會停止;若flag使用volatile修飾,第一個線程會立刻感知到flag的修改,跳出while循環

1.2 原子性

在一次或多次操作中,要麼所有操作都執行成功,要麼所有操作都不執行

原子性演示:

import java.util.ArrayList;
import java.util.List;

/**
 * 演示原子性
 */
public class AtomicityDemo {

    // 共享變量
    private static int num = 0;

    public static void main(String[] args) throws InterruptedException {

        // 線程任務
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                num++;
            }
        };

        List<Thread> list = new ArrayList<>();

        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(task);
            t.start();
            list.add(t);
        }

        for (Thread t : list)
            t.join();

        System.err.println(num);

    }

}

上述代碼的輸出,大多數情況下是小於50_000的結果,反彙編後,可以看到lambd表達式中num++對應的操作是:

9 : getstatic     #14                 // Field num:I
12: iconst_1
13: iadd
14: putstatic     #14                 // Field num:I

也就是說,一個線程進行num++操作時,要經過獲取靜態變量值準備常量1相加對靜態變量重新賦值4個步驟;當一個線程執行iadd後,若還沒有重新對靜態變量賦值就被掛起,切換到另一個線程對num進行操作,就會產生線程操作結果被覆蓋的結果

1.3 有序性

指程序中代碼的執行順序,java在編譯和運行時會對代碼進行優化,導致程序最終執行順序不一定就是代碼編寫時的順序

2 JMM:java內存模型

jmm規範描述了java程序中各種變量的訪問規則,以及在jvm中變量在主存和工作內存間交互的細節,提供了可見性、有序性和原子性的保障,通過synchronized和volatile關鍵字實現

2.1 主內存與工作內存

  • 主內存,線程共享,所有的共享變量都存儲與主內存中
  • 工作內存,每個線程都有自己的工作內存,其只存儲對共享變量的的副本;線程對變量的操作都必須在工作內存中完成,不能直接讀寫主內存中的變量;不同線程也不能訪問彼此工作內存中的數據

在這裏插入圖片描述

2.2 內存交互

內存模型定義了一系列原子的、不可再分的操作,實現主內存與工作內存之間的交互

八種內存交互操作:

  1. lock,作用於主內存變量,將一個變量標識爲某個線程獨佔的狀態
  2. unlock,作用於主內存變量,將一個處於鎖定狀態的變量釋放出來,使得該變量可以被其它線程鎖定
  3. read,作用於主內存變量,用於將一個變量從主內存傳輸到線程的工作內存,以便load動作使用
  4. load,作用於工作內存變量,用於將read到的變量值放入工作內存中的變量副本
  5. use,作用於工作內存變量,把工作內存中的一個變量的值傳遞給執行引擎;每當虛擬機遇到一個需要使用變量值的字節碼指令時,都會執行這個操作
  6. assign,作用於工作內存變量,把一個從執行引擎接受的值賦給工作內存中的變量;每當虛擬機遇到一個需要給變量賦值的字節碼指令時,會執行這個操作
  7. store,作用於工作內存變量,將工作內存中的一個變量值傳遞到主內存中,以便write操作使用
  8. write,作用於主內存變量,將store操作後的值存入主內存

3 synchronized保證三個特性

3.1 可見性

對於1.1中代碼,可以做如下修改來達到可見性:

/**
 * 通過synchronized關鍵字保證可見性
 */
public class VisibilityDemo {

    // 共享變量
    private static boolean flag = true;
	
	// 鎖
	private static final Object object = new Object();

    public static void main(String[] args) throws InterruptedException {

        // 1個線程不斷讀取共享變量
        new Thread(() -> {
            while (flag) {
            	// 添加一個空的同步代碼塊
            	synchronized(object) {}
            }
            System.err.println("while ended");
        }).start();

        Thread.sleep(2_000);

        // 1個線程修改共享變量
        new Thread(() -> flag = false).start();

    }

}

在while循環中使用了synchronized同步代碼塊,synchronized關鍵字在每次進出時會分別調用內存模型中的lockunlock操作,進行了讀取變量到工作內存和同步工作線程中的變量到主內存中的工作,從而保證了可見性,即能夠立刻讀取到被修改的flag變量值

3.2 原子性

通過monitorentermonitorexit保證

3.3 有序性

爲什麼要重排序:編譯器及cpu在一些時候爲了提高程序的執行效率,會對代碼的執行順序重新編排

as-if-serial:不論編譯器和cpu如何進行重排序,必須保證單線程情況下,程序執行結果的正確性

不能重排序的幾種情況

  • 寫後讀

      int a = 1;
      int b = a;
    
  • 寫後寫

      int a = 1;
      a = 2;
    
  • 讀後寫

      int a = 1;
      int b = a;
      a = 2;
    

synchronized如何保證重排序不影響多線程下的執行結果synchronized關鍵字通過保證被其修飾的代碼只在單線程下執行,且具有原子性,所以即使發生了重排序,這個同步塊的執行結果仍然不會發生改變,即as-if-serial,從而不會影響多線程下的執行結果

4 synchronized特性

4.1 可重入性

可重入性,指一個線程可以重複進入同一把鎖的多個代碼塊;

在synchronized關鍵字的實現上,有一個計數器(recursion變量)會記錄線程獲得過幾次鎖,只有該計數器清零後才能順利釋放對應的鎖;

其具有可重入性,有如下幾點好處:

  • 避免死鎖
  • 有利於封裝代碼

4.2 不可中斷

不可中斷性,指當某個線程想要獲取已經被其它線程持有的鎖時,必須處於阻塞狀態,直到獲取到鎖爲止,期間不可被中斷

於synchronized關鍵字相比,顯式鎖Lock能夠實現中斷鎖,更加靈活

5 synchronized原理

5.1 反彙編同步代碼塊和同步方法

示例代碼如下:

public class Demo {

    private static Object obj = new Object();

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

    public synchronized void test() {
        System.out.println("synchronized method");
    }

}

對上述代碼的字節碼文件進行反彙編後,先關注main方法的反彙編結果:

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: getstatic     #2                  // Field obj:Ljava/lang/Object;
         3: dup
         4: astore_1
--------------------------------------------------------------------------------------------------------
         5: monitorenter
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String synchronized block
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
--------------------------------------------------------------------------------------------------------
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 14
        line 11: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

上述代碼塊中,兩條虛線間的代碼對應着源碼main方法中同步代碼塊執行過程,其中主要標識爲monitorentermonitorexit,下面是對兩個關鍵字的解釋:

  • monitorenter

    虛擬機規範中的描述:

    The objectref must be of type reference.

    Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

    • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

    • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

    • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

    objectref必須是一個引用類型,每個對象都與一個監視器相關聯。當且僅當它有一個所有者時,監視器才被鎖定。執行monitorenter的線程試圖獲得與objectref關聯的監視器的所有權:

    • monitorentry count爲0時,線程可以進入monito並將entry count設置爲1,該線程變爲其持有者
    • 若線程已經是monitor的持有者,則重入monitorentry count加1
    • 若其它線程已經是monitor的持有者,那麼當前線程進入阻塞狀態,直到monitorentry count爲0時,當前線程纔可以再次嘗試獲取
  • monitorexit

    虛擬機規範中的描述:

    The objectref must be of type reference.

    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

    The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

    objectref必須是一個引用類型,並且執行monitorexit的線程必須是monitor的持有者。線程會將monitorentry count減1,若減1後的結果爲0,線程離開monitor,不再是其持有者,這時其它阻塞的想要獲取該monitor的線程允許嘗試獲取該monitor

最後需要注意的是,反編譯後的結果中,有兩個monitorexit,這是因爲當有異常發生時,會自動釋放鎖

然後關注同步方法test()反編譯結果:

public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, 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 synchronized method
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/my4tb/concurrent/synchronization/Demo;

同步方法在運行時常量池的method_info結構中由ACC_SYNCHRONIZED標誌加以區分,該標誌由方法調用指令進行檢查。方法級同步是隱式執行的,作爲方法調用和返回的一部分

5.2 monitor對象

hotpot虛擬機中,monitor對象的實現,是通過ObjectMonitor完成的。其源碼爲c++實現,可以在ObjectMonitor.hpp的構造器中看到其主要數據結構如下,並對幾個關鍵參數做出註釋:

class ObjectMonitor {
	...
	// 構造器
	ObjectMonitor() {
		_header					= NULL;
		_count					= 0;
		_waiters				= 0;
		_recursions				= 0;	// 線程重入次數
		_object					= NULL;	// 存儲指向當前monitor的對象的引用
		_owner					= NULL;	// 持有該monitor的線程
		_WaitSet				= NULL;	// 調用java中wait()方法後,存儲在java中狀態是blocked的線程,
		_WaitSetLock			= 0;
		_Responsible			= NULL;
		_succ					= NULL;
		_cxq					= NULL;	// 多線程競爭鎖時,排隊的單向鏈表
		FreeNext				= NULL;
		_EntryList				= NULL;	// 調用java中notify()或notifyAll()方法後,存儲在java中是狀態是Runnable的線程
		_SpinFreq				= 0;
		_SpinClock				= 0;
		OwnerIsThread			= 0;
		_previous_owner_tid		= 0;
	}
	...
}

5.3 爲什麼說monitor是重量級鎖

虛擬機內部對monitor對象進行操作時,會涉及調用很多內核函數,如線程競爭鎖失敗時調用的park()函數;在調用內核函數時,會在os的用戶態和內核態進行切換,這種切換操作會消耗大量系統資源

6 synchronized優化

jdk1.6對synchronized做了很多優化

6.1 鎖升級過程

無鎖 - 偏向鎖 - 輕量級鎖 - 重量級鎖

6.2 java對象內存佈局(hotpot虛擬機)

java對象在堆內存中的存儲佈局可以分爲如下三個部分:

  • 對象頭(header)
  • 實例數據(instance data)
  • 對齊填充(padding)

6.2.1 對象頭

對象頭又分爲兩部分,分別是:

  • mark word,用於存儲對象自身的運行時數據,如哈希碼、gc分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程id及偏向時間戳等;這部分數據長度爲32位(32虛擬機)或64位(64位虛擬機);mark word的信息存儲是動態的,如下:

    |------------------------------------------------------------------------------|--------------------|
    |                                  Mark Word (64 bits)                         |       State        |
    |------------------------------------------------------------------------------|--------------------|
    | unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
    |------------------------------------------------------------------------------|--------------------|
    | thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
    |------------------------------------------------------------------------------|--------------------|
    |                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    |                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
    |------------------------------------------------------------------------------|--------------------|
    |                                                                     | lock:2 |    Marked for GC   |
    |------------------------------------------------------------------------------|--------------------|
    
  • klass pointer,是對象指向它的類型元數據的指針,從而確定該對象屬於哪個類的實例;這部分數據長度爲32位(32虛擬機)或64位(64位虛擬機)

6.2.2 實例數據

這部分存儲對象中的有效信息

6.2.3 對齊填充

hotpot虛擬機自動內存管理系統要求對象的起始地址必須是8字節的整倍數,因此在對象不足8字節整倍數的情況下,需要這部分湊夠

6.3 偏向鎖

單個線程多次獲取同一把鎖

爲什麼設置偏向鎖:大多數情況下,鎖是不會被多線程競爭的,總是由同一個線程多次獲得;因爲爲了這種情況下線程獲取鎖的代價更低,引入了偏向鎖

偏向鎖加鎖過程:鎖對象第一次被線程獲取時,虛擬機會將mark word中鎖標誌位保持爲01,然後將偏向狀態由0設置爲1,表示進入偏向模式;同時通過cas操作,將獲取到鎖的線程id及epoch設置在mark word中。

持有偏向鎖的線程工作:持有偏向鎖的線程以後在每次進入該鎖的同步代碼塊時,只需要比較當前線程id和mark word中存儲的線程id即可,除此之外虛擬機將不會進行任何同步操作

偏向鎖撤銷:當有其它線程來嘗試獲取鎖的時候,偏向模式會立刻宣告結束;恢復偏向模式爲0後,根據不同情況,鎖標誌位可能變爲01(無鎖)或00(輕量級鎖)

6.4 輕量級鎖

多個線程交替獲取鎖,而非競爭鎖

爲什麼設置輕量級鎖:輕量是相對於monitor重量鎖而言的,在沒有多線程競爭鎖的前提下,減少重量級鎖所帶來的資源消耗

輕量級鎖加鎖過程

  1. 代碼即將進入同步代碼塊時,若此時同步對象沒有被鎖定(對象mark word中的鎖狀態爲01),虛擬機首先會在當前線程的棧幀中建立一個鎖記錄(lock record),用於存儲同步對象的mark word拷貝(hash code、age、鎖標記-01)
  2. 然後虛擬機通過cas操作嘗試把對象的mard word更新爲指向lock record的指針
  3. 若更新成功,則代表線程持有鎖對象,並修改mark word中的鎖標誌位修改爲00
  4. 若更新失敗,虛擬機會檢查mark word中是否指向當前線程的棧幀,若是,說明線程已經持有鎖,直接進入同步代碼塊
  5. 若不是,多個線程競爭鎖,則同步對象鎖狀態變爲10,mark word存儲指向重量級鎖monitor的指針,獲取鎖失敗的線程進入阻塞

輕量級鎖解鎖過程:同樣使用cas,將棧幀中的lock record數據替換回同步對象的mark word中

6.5 自旋鎖與適應性自旋鎖

爲什麼設置自旋鎖:多數情況下,共享數據的鎖定狀態只會持續很短的時間,這個時間要比線程的掛起和恢復(轉入內核態完成)的時間要短;若多線程能夠並行執行,可以讓後來線程自旋等待,避免了線程切換的開銷

適應性:自旋時間不再固定,而是參考上一次在同一個鎖對象的自旋時間以及鎖擁有者的狀態來決定

6.6 鎖消除

指jvm即時編譯器在運行時,對一些代碼要求同步,但是對於某些不可能存在競爭的共享數據的鎖進行消除;如果判斷到一段代碼中,在堆上所有的數據都不會逃逸出去(逃逸分析)被其它線程所訪問,那就可以把它們當作棧上數據對待,認爲它們是線程私有的,同步加鎖無需進行

6.7 鎖粗化

若一系列連續操作都是對同一個對象反覆加鎖解鎖,甚至加鎖操作是出現在循環體中,那麼頻繁的同步互斥操作會導致不必要的性能消耗;這種情況下,虛擬機會將同步操作範圍擴展(粗化)到整個操作序列的外部

7 面試題:Lock和synchronized的區別

  • Lock是一個接口,而synchronized是一個關鍵字
  • Lock必須手動釋放鎖,synchronized會自動釋放鎖
  • Lock可以響應中斷,synchronized不能響應中斷
  • Lock只能鎖住方法中的代碼塊,synchronized不僅能鎖住代碼塊,還能鎖住方法
  • Lock可以實現公平鎖和非公平鎖,synchronized是非公平鎖
  • Lock有共享鎖和獨佔鎖的實現,synchronized是獨佔鎖
  • Lock可以顯示的獲取線程是否獲取到鎖了,synchronized不能
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章