您現在的位置是:網站首頁> 內容頁

Java并發(三):synchronized實現原理

  • 沐鳴平臺注冊登錄通道
  • 2019-10-02
  • 474人已閱讀
簡介一、synchronized用法Java中的同步塊用synchronized標記。同步塊在Java中是同步在某個對象上(監視器對象)。所有同步在一個對象上的同步塊在同時只能被一個線程進

一、synchronized用法

Java中的同步塊用synchronized標記。

同步塊在Java中是同步在某個對象上(監視器對象)。

所有同步在一個對象上的同步塊在同時只能被一個線程進入并執行操作。

所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊中的線程退出。

(注:不要使用全局對象(常量等)做監視器。應使用唯一對應的對象)

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(int value){ count += value; } // 4.靜態方法中的同步塊 (等價于3) public static void add(int value){ synchronized(MyClass.class){ count += value; } }}

二、Java對象模型

每一個Java類,在被JVM加載的時候,JVM會給這個類創建一個instanceKlass,保存在方法區,用來在JVM層表示該Java類。

使用new創建一個對象的時候,JVM會創建一個instanceOopDesc對象,這個對象中包含了兩部分信息,對象頭以及實例數據。

對象頭中包括兩部分:(一)Mark Word 一些運行時數據,其中就包括和多線程相關的鎖的信息。

         ?。ǘ㎏lass Point 元數據其實維護的是指針,指向的是對象所屬的類的instanceKlass。

Mark Word?用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等等。

對象頭信息是與對象自身定義的數據無關的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的數據,它會根據對象的狀態復用自己的存儲空間,也就是說,Mark Word會隨著程序的運行發生變化。

對象存儲結構:

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

三、Moniter

為了解決線程安全的問題,Java提供了同步機制、互斥鎖機制,這個機制保證了在同一時刻只有一個線程能訪問共享資源。這個機制的保障來源于監視鎖Monitor。

每一個Object對象中內置了一個Monitor對象。(對象頭的MarkWord中的LockWord指向monitor的起始地址)

Monitor相當于一個許可證,線程拿到許可證即可以進行操作,沒有拿到則需要阻塞等待。

ObjectMonitor中有幾個關鍵屬性:

  _owner:指向持有ObjectMonitor對象的線程

  _WaitSet:存放處于wait狀態的線程隊列

  _EntryList:存放處于等待鎖block狀態的線程隊列

  _recursions:鎖的重入次數

  _count:用來記錄該線程獲取鎖的次數

線程T等待對象鎖:_EntryList中加入T

線程T獲取對象鎖:_EntryList移除T,_owner置為T,計數器_count+1

持有對象鎖的線程調用wait():_owner置為T,計數器_count+1,_WaitSet中加入T

?

四、synchronized原理

利用javap工具查看生成的class文件信息來分析Synchronize的實現?

(摘自:?【死磕Java并發】—–深入分析synchronized的實現原理)

public class SynchronizedTest { public synchronized void test1(){ } public void test2(){ synchronized (this){ } }}

?

同步代碼塊:monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結束位置,JVM需要保證每一個monitorenter都有一個monitorexit與之相對應。任何對象都有一個monitor與之相關聯,當且一個monitor被持有之后,他將處于鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor所有權,即嘗試獲取對象的鎖;?同步方法:synchronized方法則會被翻譯成普通的方法調用和返回指令如:invokevirtual、areturn指令,在VM字節碼層面并沒有任何特別的指令來實現被synchronized修飾的方法,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標志位置1,表示該方法是同步方法并使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Klass做為鎖對象。

五、synchronized解決并發問題

synchronized保證原子性

1)通過monitorentermonitorexit指令,可以保證synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放之前,無法被其他線程訪問到。

2)即使在執行過程中,由于某種原因,比如CPU時間片用完,線程1放棄了CPU,但是它并沒有進行解鎖。而由synchronized的鎖是可重入的,下一個時間片還是只能被他自己獲取到,還是會繼續執行代碼。直到所有代碼執行完。

synchronized保證可見性

保證可見性規則:對一個synchronized修飾的變量解鎖之前,必須先把此變量同步回主存中。

synchronized保證有序性

由于synchronized飾的代碼,同一時間只能被同一線程訪問。(如果在本線程內觀察,所有操作都是天然有序的)

synchronized是無法禁止指令重排和處理器優化的,但是同一線程內的執行遵守as-if-serial語義。

六、鎖優化

重量級鎖

synchronized其實是借助Monitor實現的,在加鎖時會調用objectMonitor的enter方法,解鎖的時候會調用exit方法。

通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴于底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。

Java的線程是映射到操作系統原生線程之上的,如果要阻塞或喚醒一個線程就需要操作系統的幫忙,這就要從用戶態轉換到核心態,因此狀態轉換需要花費很多的處理器時間,是java語言中一個重量級的操縱。

只有在JDK1.6之前,synchronized的實現才會直接調用ObjectMonitor的enterexit。在JDK1.6中出現對鎖進行了很多的優化,進而出現輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化。

自旋鎖

共享數據的鎖定狀態一般只會持續很短的一段時間,為了這段時間去掛起和恢復線程其實并不值得。

讓后面來的線程“稍微等一下”,但是并不放棄處理器的執行時間,看看持有鎖的線程會不會很快釋放鎖。這個“稍微等一下”的過程就是自旋。(怎么等待呢?執行一段無意義的循環即可)

1、由于自旋鎖只是將當前線程不停地執行循環體,不進行線程狀態的改變,所以響應速度更快。

2、但當線程數不停增加時,性能下降明顯,因為每個線程都需要執行,占用CPU時間。

3、自旋鎖和阻塞鎖最大的區別就是,到底要不要放棄處理器的執行時間。阻塞鎖是放棄了CPU時間,進入了等待區,等待被喚醒。而自旋鎖是一直“自旋”在那里,時刻的檢查共享資源是否可以被訪問。

鎖消除

JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。鎖消除的依據是逃逸分析的數據支持。?

注意:我們在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,這個時候會存在隱形的加鎖操作。

// 在運行這段代碼時,JVM可以明顯檢測到變量vector沒有逃逸出方法vectorTest()之外,所以JVM可以大膽地將vector內部的加鎖操作消除。 public void vectorTest() { Vector<String> vector = new Vector<String>(); for (int i = 0; i < 10; i++) { vector.add(i + "");// vector是線程安全的,每個方法都有synchronized修飾 } System.out.println(vector); }

鎖粗化

我們提倡盡量減小鎖的粒度:使用同步鎖的時候,需要讓同步塊的作用范圍盡可能小—僅在共享數據的實際作用域中才進行同步,這樣做的目的是為了使需要同步的操作數量盡可能縮小,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。

問題:如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗。

鎖粗化:將多個連續的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖。

for(int i=0;i<100000;i++){ synchronized(this){ do(); } // 被優化之后 synchronized(this){ for(int i=0;i<100000;i++){ do(); }

輕量級鎖

引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

只有在“對于絕大部分的鎖,在整個生命周期內都是不會存在競爭的”情況下,輕量級鎖才有較好的性能。

如果存在競爭的情況,輕量級鎖需要膨脹為重量級鎖,而且還會有額外的CAS操作,會比重量級鎖性能更差。

正常獲取鎖過程:

1)當前線程的棧幀中建立一個的空間Lock Record,將鎖對象的Mark Word的拷貝過來

2)JVM利用CAS操作將對象的Mark Word更新為指向Lock Record的指針,如果成功表示競爭到鎖

3)直接執行同步塊代碼,不需要monitor

獲取鎖所有情況

1)判斷當前對象是否處于無鎖狀態(hashcode、0、01)

  無鎖狀態:JVM首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,執行(2)

  有鎖:執行步驟(3);

2)JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針

  更新指針成功:表示競爭到鎖,則將鎖標志位變成00(表示此對象處于輕量級鎖狀態),直接執行同步塊代碼,不需要monitor,結束;

  更新指針成功:未競爭到鎖,執行步驟(3);

3)判斷當前對象的Mark Word是否指向當前線程的棧幀,

  指向當前線程的棧幀:表示當前線程已經持有當前對象的鎖,則直接執行同步代碼塊,不需要monitor,結束;

  不指向當前線程的棧幀:說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖標志位變成10,后面等待的線程將會進入阻塞狀態;

釋放鎖 :

1)取出在獲取輕量級鎖保存在Displaced Mark Word中的數據;

2)用CAS操作將取出的數據替換當前對象的Mark Word中,如果成功,則說明釋放鎖成功,否則執行(3);

3)如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的線程。

偏向鎖

引入偏向鎖主要目的是:為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑。

獲取鎖

1)檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖標識位為01;

2)若為可偏向狀態,則測試線程ID是否為當前線程ID,如果是,則執行步驟(5),否則執行步驟(3);

3)如果線程ID不為當前線程ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的線程ID替換為當前線程ID,否則執行線程(4);

4)通過CAS競爭鎖失敗,證明當前存在多線程競爭情況,當到達全局安全點,獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續往下執行同步代碼塊;

5)執行同步代碼塊

釋放鎖?

偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(這個時間點是上沒有正在執行的代碼)。其步驟如下:

1)暫停擁有偏向鎖的線程,判斷鎖對象石是否還處于被鎖定狀態;

2)撤銷偏向蘇,恢復到無鎖狀態(01)或者輕量級鎖的狀態;

?

?

參考資料:

【死磕Java并發】—–深入分析synchronized的實現原理

深入理解多線程(一)——Synchronized的實現原理

再有人問你Java內存模型是什么,就把這篇文章發給他。

文章評論

Top 七彩娱乐安卓