簡介
synchronized 是Java提供的一個并發控制的關鍵字,主要有兩種用法:分别是同步方法和同步代碼塊,也就是說 synchronized 既可以修飾方法也可以修飾代碼塊。當我們想要保證一個共享資源在同一時間隻會被一個線程通路到時,可以在代碼中使用 synchronized 關鍵字對類或者對象加鎖。
被 synchronized 修飾的代碼塊及方法,在同一時間,隻能被單個線程通路。
public class SynchronizedDemo {
//同步方法
public synchronized void doSth(){
System.out.println("Hello World");
}
//同步代碼塊
public void doSth1(){
synchronized (SynchronizedDemo.class){
System.out.println("Hello World");
}
}
}
通過反編譯後的代碼得知 :
于同步方法,JVM(JVM是 Java虛拟機)采用
ACC_SYNCHRONIZED
标記符來實作同步。
對于同步代碼塊,JVM采用
monitorenter
、
monitorexit
兩個指令來實作同步。
方法級的同步是隐式的。同步方法的常量池中會有一個
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方法。
synchronized與原子性
原子性是指一個操作是不可中斷的,要全部執行完成,要不就都不執行。
線程是CPU排程的基本機關。CPU有時間片的概念,會根據不同的排程算法進行線程排程。當一個線程獲得時間片之後開始執行,在時間片耗盡之後,就會失去CPU使用權。是以在多線程場景下,由于時間片線上程間輪換,就會發生原子性問題。
為了保證原子性 Java 提供兩個進階的位元組碼指令
monitorenter
monitorexit(
對應的關鍵字就是
synchronized)
。
通過
monitorenter
monitorexit
指令,可以保證被
synchronized
修飾的代碼在同一時間隻能被一個線程通路,在鎖未釋放之前,無法被其他線程通路到。在Java中可以使用
synchronized
來保證方法和代碼塊内的操作是原子性的。
線程1在執行
monitorenter
指令的時候,會對Monitor進行加鎖,加鎖後其他線程無法獲得鎖,除非線程1主動解鎖。即使在執行過程中,由于某種原因,比如CPU時間片用完,線程1放棄了CPU,但是,他并沒有進行解鎖。而由于
synchronized
的鎖是可重入的,下一個時間片還是隻能被他自己擷取到,還是會繼續執行代碼。直到所有代碼執行完。這就保證了原子性。
synchronized與可見性
可見性是指當多個線程通路同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
Java記憶體模型規定了所有的變量都存儲在主記憶體中,每條線程還有自己的工作記憶體,線程的工作記憶體中儲存了該線程中是用到的變量的主記憶體副本拷貝,線程對變量的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。不同的線程之間也無法直接通路對方工作記憶體中的變量,線程間變量的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。是以,就可能出現線程1改了某個變量的值,但是線程2不可見的情況。被
synchronized
修飾的代碼,在開始執行時會加鎖,執行完成後會進行解鎖。
而為了保證可見性,有一條規則是這樣的:對一個變量解鎖之前,必須先把此變量同步回主存中。這樣解鎖後,後續線程就可以通路到被修改後的值。
synchronized關鍵字鎖住的對象,其值是具有可見性的。
synchronized與有序性
有序性即程式執行的順序按照代碼的先後順序執行。
除了引入了時間片以外,由于處理器優化和指令重排等,CPU還可能對輸入代碼進行亂序執行,比如load->add->save 有可能被優化成load->save->add 。這就是可能存在有序性問題。
注意:
synchronized
是無法禁止指令重排和處理器優化的(
synchronized
無法避免上述提到的問題)。