再有人問你synchronized是什麼,就把這篇文章發給他

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的區別等。敬請期待。


分享到:


相關文章: