在再有人問你Java記憶體模型是什麼,就把這篇文章發給他。中我們曾經介紹過,Java語言為了解決併發程式設計中存在的原子性、可見性和有序性問題,提供了一系列和併發處理相關的關鍵字,比如synchronized
、volatile
、final
、concurren包
等。
在《深入理解Java虛擬機器》中,有這樣一段話:
synchronized
關鍵字在需要原子性、可見性和有序性這三種特性的時候都可以作為其中一種解決方案,看起來是“萬能”的。的確,大部分併發控制操作都能使用synchronized來完成。
海明威在他的《午後之死》說過的:“冰山運動之雄偉壯觀,是因為他只有八分之一在水面上。”對於程式設計師來說,synchronized
只是個關鍵字而已,用起來很簡單。之所以我們可以在處理多執行緒問題時可以不用考慮太多,就是因為這個關鍵字幫我們遮蔽了很多細節。
那麼,本文就圍繞synchronized
展開,主要介紹synchronized
的用法、synchronized
的原理,以及synchronized
是如何提供原子性、可見性和有序性保障的等。
synchronized的用法
synchronized
是Java提供的一個併發控制的關鍵字。主要有兩種用法,分別是同步方法和同步程式碼塊。也就是說,synchronized
既可以修飾方法也可以修飾程式碼塊。
/**
* @author Hollis 18/08/04.
*/
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}
//同步程式碼塊
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}
複製程式碼
被synchronized
修飾的程式碼塊及方法,在同一時間,只能被單個執行緒訪問。
synchronized的實現原理
synchronized
,是Java中用於解決併發情況下資料同步訪問的一個很重要的關鍵字。當我們想要保證一個共享資源在同一時間只會被一個執行緒訪問到時,我們可以在程式碼中使用synchronized
關鍵字對類或者物件加鎖。
在深入理解多執行緒(一)——Synchronized的實現原理中我曾經介紹過其實現原理,為了保證知識的完整性,這裡再簡單介紹一下,詳細的內容請去原文閱讀。
我們對上面的程式碼進行反編譯,可以得到如下程式碼:
public synchronized void doSth();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #5 // class com/hollis/SynchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String Hello World
10: invokevirtual #4 // 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
複製程式碼
通過反編譯後程式碼可以看出:對於同步方法,JVM採用ACC_SYNCHRONIZED
標記符來實現同步。 對於同步程式碼塊。JVM採用monitorenter
、monitorexit
兩個指令來實現同步。
在The Java® Virtual Machine Specification中有關於同步方法和同步程式碼塊的實現原理的介紹,我翻譯成中文如下:
方法級的同步是隱式的。同步方法的常量池中會有一個
ACC_SYNCHRONIZED
標誌。當某個執行緒要訪問某個方法的時候,會檢查是否有ACC_SYNCHRONIZED
,如果有設定,則需要先獲得監視器鎖,然後開始執行方法,方法執行之後再釋放監視器鎖。這時如果其他執行緒來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。值得注意的是,如果在方法執行過程中,發生了異常,並且方法內部並沒有處理該異常,那麼在異常被拋到方法外面之前監視器鎖會被自動釋放。同步程式碼塊使用
monitorenter
和monitorexit
兩個指令實現。可以把執行monitorenter
指令理解為加鎖,執行monitorexit
理解為釋放鎖。 每個物件維護著一個記錄著被鎖次數的計數器。未被鎖定的物件的該計數器為0,當一個執行緒獲得鎖(執行monitorenter
)後,該計數器自增變為 1 ,當同一個執行緒再次獲得該物件的鎖的時候,計數器再次自增。當同一個執行緒釋放鎖(執行monitorexit
指令)的時候,計數器再自減。當計數器為0的時候。鎖將被釋放,其他執行緒便可以獲得鎖。
無論是ACC_SYNCHRONIZED
還是monitorenter
、monitorexit
都是基於Monitor實現的,在Java虛擬機器(HotSpot)中,Monitor是基於C++實現的,由ObjectMonitor實現。
ObjectMonitor類中提供了幾個方法,如enter
、exit
、wait
、notify
、notifyAll
等。sychronized
加鎖的時候,會呼叫objectMonitor的enter方法,解鎖的時候會呼叫exit方法。(關於Monitor詳見深入理解多執行緒(四)—— Moniter的實現原理)
synchronized與原子性
原子性是指一個操作是不可中斷的,要全部執行完成,要不就都不執行。
我們在Java的併發程式設計中的多執行緒問題到底是怎麼回事兒?中分析過:執行緒是CPU排程的基本單位。CPU有時間片的概念,會根據不同的排程演算法進行執行緒排程。當一個執行緒獲得時間片之後開始執行,在時間片耗盡之後,就會失去CPU使用權。所以在多執行緒場景下,由於時間片線上程間輪換,就會發生原子性問題。
在Java中,為了保證原子性,提供了兩個高階的位元組碼指令monitorenter
和monitorexit
。前面中,介紹過,這兩個位元組碼指令,在Java中對應的關鍵字就是synchronized
。
通過monitorenter
和monitorexit
指令,可以保證被synchronized
修飾的程式碼在同一時間只能被一個執行緒訪問,在鎖未釋放之前,無法被其他執行緒訪問到。因此,在Java中可以使用synchronized
來保證方法和程式碼塊內的操作是原子性的。
執行緒1在執行
monitorenter
指令的時候,會對Monitor進行加鎖,加鎖後其他執行緒無法獲得鎖,除非執行緒1主動解鎖。即使在執行過程中,由於某種原因,比如CPU時間片用完,執行緒1放棄了CPU,但是,他並沒有進行解鎖。而由於synchronized
的鎖是可重入的,下一個時間片還是隻能被他自己獲取到,還是會繼續執行程式碼。直到所有程式碼執行完。這就保證了原子性。
synchronized與可見性
可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。
我們在再有人問你Java記憶體模型是什麼,就把這篇文章發給他。中分析過:Java記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒中是用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。所以,就可能出現執行緒1改了某個變數的值,但是執行緒2不可見的情況。
前面我們介紹過,被synchronized
修飾的程式碼,在開始執行時會加鎖,執行完成後會進行解鎖。而為了保證可見性,有一條規則是這樣的:對一個變數解鎖之前,必須先把此變數同步回主存中。這樣解鎖後,後續執行緒就可以訪問到被修改後的值。
所以,synchronized關鍵字鎖住的物件,其值是具有可見性的。
synchronized與有序性
有序性即程式執行的順序按照程式碼的先後順序執行。
我們在再有人問你Java記憶體模型是什麼,就把這篇文章發給他。中分析過:除了引入了時間片以外,由於處理器優化和指令重排等,CPU還可能對輸入程式碼進行亂序執行,比如load->add->save 有可能被優化成load->save->add 。這就是可能存在有序性問題。
這裡需要注意的是,synchronized
是無法禁止指令重排和處理器優化的。也就是說,synchronized
無法避免上述提到的問題。
那麼,為什麼還說synchronized
也提供了有序性保證呢?
這就要再把有序性的概念擴充套件一下了。Java程式中天然的有序性可以總結為一句話:如果在本執行緒內觀察,所有操作都是天然有序的。如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。
以上這句話也是《深入理解Java虛擬機器》中的原句,但是怎麼理解呢?周志明並沒有詳細的解釋。這裡我簡單擴充套件一下,這其實和as-if-serial語義
有關。
as-if-serial
語義的意思指:不管怎麼重排序(編譯器和處理器為了提高並行度),單執行緒程式的執行結果都不能被改變。編譯器和處理器無論如何優化,都必須遵守as-if-serial
語義。
這裡不對as-if-serial語義
詳細展開了,簡單說就是,as-if-serial語義
保證了單執行緒中,指令重排是有一定的限制的,而只要編譯器和處理器都遵守了這個語義,那麼就可以認為單執行緒程式是按照順序執行的。當然,實際上還是有重排的,只不過我們無須關心這種重排的干擾。
所以呢,由於synchronized
修飾的程式碼,同一時間只能被同一執行緒訪問。那麼也就是單執行緒執行的。所以,可以保證其有序性。
synchronized與鎖優化
前面介紹了synchronized
的用法、原理以及對併發程式設計的作用。是一個很好用的關鍵字。
synchronized
其實是藉助Monitor實現的,在加鎖時會呼叫objectMonitor的enter
方法,解鎖的時候會呼叫exit
方法。事實上,只有在JDK1.6之前,synchronized的實現才會直接呼叫ObjectMonitor的enter
和exit
,這種鎖被稱之為重量級鎖。
所以,在JDK1.6中出現對鎖進行了很多的優化,進而出現輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化(自旋鎖在1.4就有,只不過預設的是關閉的,jdk1.6是預設開啟的),這些操作都是為了線上程之間更高效的共享資料 ,解決競爭問題。
關於自旋鎖、鎖粗化和鎖消除可以參考深入理解多執行緒(五)—— Java虛擬機器的鎖優化技術,關於輕量級鎖和偏向鎖,已經在排期規劃中,我後面會有文章單獨介紹,將獨家釋出在我的部落格(http://www.hollischuang.com)和公眾號(Hollis)中,敬請期待。
好啦,關於synchronized
關鍵字,我們介紹了其用法、原理、以及如何保證的原子性、順序性和可見性,同時也擴充套件的留下了鎖優化相關的資料及思考。後面我們會繼續介紹volatile
關鍵字以及他和synchronized
的區別等。敬請期待。