在再有人問你Java內存模型是什麼,就把這篇文章發給他。中咱們曾經介紹過,Java語言爲了解決併發編程中存在的原子性、可見性和有序性問題,提供了一系列和併發處理相關的關鍵字,好比synchronized
、volatile
、final
、concurren包
等。html
在《深刻理解Java虛擬機》中,有這樣一段話:java
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
,是Java中用於解決併發狀況下數據同步訪問的一個很重要的關鍵字。當咱們想要保證一個共享資源在同一時間只會被一個線程訪問到時,咱們能夠在代碼中使用synchronized
關鍵字對類或者對象加鎖。oracle
在深刻理解多線程(一)——Synchronized的實現原理中我曾經介紹過其實現原理,爲了保證知識的完整性,這裏再簡單介紹一下,詳細的內容請去原文閱讀。jvm
咱們對上面的代碼進行反編譯,能夠獲得以下代碼:優化
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的實現原理)
原子性是指一個操做是不可中斷的,要所有執行完成,要不就都不執行。
咱們在Java的併發編程中的多線程問題究竟是怎麼回事兒?中分析過:線程是CPU調度的基本單位。CPU有時間片的概念,會根據不一樣的調度算法進行線程調度。當一個線程得到時間片以後開始執行,在時間片耗盡以後,就會失去CPU使用權。因此在多線程場景下,因爲時間片在線程間輪換,就會發生原子性問題。
在Java中,爲了保證原子性,提供了兩個高級的字節碼指令monitorenter
和monitorexit
。前面中,介紹過,這兩個字節碼指令,在Java中對應的關鍵字就是synchronized
。
經過monitorenter
和monitorexit
指令,能夠保證被synchronized
修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放以前,沒法被其餘線程訪問到。所以,在Java中可使用synchronized
來保證方法和代碼塊內的操做是原子性的。
線程1在執行
monitorenter
指令的時候,會對Monitor進行加鎖,加鎖後其餘線程沒法得到鎖,除非線程1主動解鎖。即便在執行過程當中,因爲某種緣由,好比CPU時間片用完,線程1放棄了CPU,可是,他並無進行解鎖。而因爲synchronized
的鎖是可重入的,下一個時間片仍是隻能被他本身獲取到,仍是會繼續執行代碼。直到全部代碼執行完。這就保證了原子性。
可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
咱們在再有人問你Java內存模型是什麼,就把這篇文章發給他。中分析過:Java內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間進行數據同步進行。因此,就可能出現線程1改了某個變量的值,可是線程2不可見的狀況。
前面咱們介紹過,被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
實際上是藉助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
的區別等。敬請期待。