synchronized知識
在談論synchronized之前,我們需要了解線程安全問題的主要誘因。線程安全問題的主要誘因如下:
- 存在共享數據(也稱為臨界資源)
- 存在多條線程共同操作這些共享數據
而解決線程安全的根本方法就是:同一時刻有且只有一個線程在操作共享數據,其他線程必須等到該線程處理完數據后再對共享數據進行操作。
基於上述,引入了互斥鎖,其具有兩個特性:
- 互斥性:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現多線程的協調機制,這樣在同一時間只有一個線程對需要同步的代碼塊進行訪問。互斥性也稱作操作的原子性。
- 可見性:必須確保在鎖被釋放之前,對共享變量所做的修改,對於隨后獲得該鎖的另一個線程是可見的(即在獲得鎖時應獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續操作,從而引起不一致。
需要注意的是,synchronized鎖的不是代碼,而是對象。
根據獲取鎖的分類,可分為獲取對象鎖、獲取類鎖。
獲取對象鎖的兩種方法:
1、同步代碼塊(synchronized(this),synchronized(類實例對象)),鎖是小括號中的實例對象。
2、同步非靜態方法(synchronized method),鎖是當前對象的實例對象。
獲取類鎖的兩種方法:
1、同步代碼塊(synchronized(類.class)),鎖是小括號中的類的對象。
2、同步靜態方法(synchronized static method),鎖是當前對象的類對象(class對象)。
接下來,對對象鎖和類鎖進行總結對比:
1、有線程訪問對象的同步代碼塊時,另外的線程可以訪問該對象的非同步代碼塊;
2、若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時,另一個訪問對象的同步代碼塊的線程會被阻塞;
3、若鎖住的是同一個對象,一個線程在訪問對象的同步方法時,另一個訪問對象的同步方法的線程會被阻塞;
4、若鎖住的是同一個對象,一個線程在訪問對象的同步代碼塊時,另一個訪問對象的同步方法的線程會被阻塞,反之亦然;
5、同一類的不同對象的對象鎖互不干擾;
6、類鎖由於也是一種特殊的對象鎖,因此表現和上述的1、2、3、4一致,而由於一個類只有一把對象鎖,所以同一類的不同對象使用類鎖將會是同步的;
7、類鎖和對象鎖互不干擾。
synchronized底層實現原理
實現synchronized的基礎:Java對象頭+Monitor
對象頭的結構如下:
虛擬機位數 | 頭對象結構 | 說明 |
32/64bit | Mark Word | 默認存儲對象的hashcode,分代年齡,鎖類型,鎖標志位等信息。 |
32/64bit | Class Metadata Address | 類型指針指向對象的類元數據,JVM通過這個指針確定該對象是哪個類的數據。 |
下面是32bit的Mark Word的說明圖:
而對於Monitor,每個Java對象天生自帶一把看不見的鎖(內部鎖/Monitor鎖)。
Monitor鎖的競爭與釋放如下圖所示:
在早期的Java版本中,synchronized屬於重量級鎖,依賴於Mutex Lock實現。線程之間的切換需要從用戶態轉換到核心態,開銷很大。
在Java 6之后,synchronized性能得到很大提升。主要是因為引入了:
1:Adaptive spinning(自適應自旋)
2:Lock Eliminate(鎖消除)
3:Lock Coarsening(鎖粗化)
4:Lightweight Locking(輕量級鎖)
5:Biased Locking(偏向鎖)
6:......
下面對這些概念進行介紹。
自旋鎖與自適應自旋鎖:
自旋鎖:在很多情況下,共享數據的鎖定狀態持續時間較短,切換線程不值得。就通過讓線程執行忙循環等待鎖的釋放,而不讓出CPU。缺點是若鎖被其他線程長時間占用,會帶來許多性能上的開銷。用戶使用preBlockspin來修改等待時間、次數,不好設定。
這樣的話,自適應自旋鎖就出現了,它自旋的次數不固定,由前一次在同一個鎖上的自旋時間及鎖的持有者的狀態來決定。
鎖消除:
JIT編譯時,對運行的上下文進行掃描,去除不可能存在競爭的鎖,節省毫無意義的請求鎖的時間。
鎖粗化:
通過擴大加鎖的范圍,避免反復加鎖和解鎖。
偏向鎖:
減少同一線程獲取鎖的代價。
大多數情況下,鎖不存在多線程競爭,總是由同一個線程多次獲得。
核心思想:如果一個線程獲得了鎖,那么鎖就進入了偏向鎖模式,此時Mark Word的結構也變為偏向結構,當該線程再次請求鎖的時候,無需做任何操作,即獲得鎖的過程只需檢查Mark Word的鎖標記位為偏向鎖以及當前線程ID等於Mark Word的ThreadID即可,這樣就省去了大量有關鎖申請的操作。
不使用與鎖競爭比較激烈的多線程場合。
輕量級鎖:
輕量級鎖是由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的情況下,當第二個線程加入鎖競爭的時候,偏向鎖就會升級為輕量級鎖。
使用場景:線程交替執行同步塊。
若存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
synchronized和ReentrantLock的區別
ReentrantLock稱為重入鎖,位於JUC包的locks,和CountDownLatch、FutureTask一樣基於AQS實現。能夠實現比synchronized更細粒度的控制,比如控制公平性。此外需要注意,調用lock()之后,必須調用unlock()釋放鎖。它的性能未必比synchronized高,並且是可重入的。
ReentrantLock公平性的設置:
ReentrantLock fairLock = new ReentrantLock(true);
參數為true時,傾向於將鎖賦予等待時間最久的線程;
公平鎖:獲取鎖的順序按先后調用lock方法的順序(慎用);
非公平鎖:搶占的順序不一定,看運氣;
synchronized是非公平鎖。
ReentrantLock能夠將鎖對象化:
判斷是否有線程,或者某個特定線程,在排隊等候獲取鎖;
帶超時的獲取鎖的嘗試;
感知有沒有成功獲取鎖。
ReentrantLock能夠將wait/notify/notifyAll對象化。
總結:
1、synchronized是關鍵字,ReentrantLock是類;
2、ReentrantLock可以獲取鎖的時間進行設置,避免死鎖;
3、ReentrantLock可以獲取各種鎖的信息;
4、ReentrantLock可以靈活實現多路通知;
5、機制:sync操作Mark Word,lock調用Unsafe類的park()方法。