synchronized 關鍵字 - 監視器鎖 monitor lock
- 5. synchronized 關鍵字 - 監視器鎖 monitor lock
- 5.1 synchronized 的特性
- 5.2 synchronized 使??例
- 5.3 Java 標準庫中的線程安全類
本節?標
? 掌握 synchronized關鍵字
5. synchronized 關鍵字 - 監視器鎖 monitor lock
(JVM 中采用的一個術語。使用鎖的過程中拋出一些異常,可能會看到 監視器鎖 這樣的報錯信息)
5.1 synchronized 的特性
(1) 互斥
synchronized 會起到互斥效果, 某個線程執?到某個對象的 synchronized 中時, 其他線程如果也執?到同?個對象 synchronized 就會阻塞等待.
? 進? synchronized 修飾的代碼塊, 相當于 加鎖
? 退出 synchronized 修飾的代碼塊, 相當于 解鎖
synchronized用的鎖是存在Java對象頭?的。
可以粗略理解成, 每個對象在內存中存儲的時候, 都存有?塊內存表?當前的 “鎖定” 狀態(類似于廁所的 “有?/??”).
如果當前是 “??” 狀態, 那么就可以使?, 使?時需要設為 “有?” 狀態.
如果當前是 “有?” 狀態, 那么其他??法使?, 只能排隊
理解 “阻塞等待”.
針對每?把鎖, 操作系統內部都維護了?個等待隊列. 當這個鎖被某個線程占有的時候, 其他線程嘗試進?加鎖, 就加不上了, 就會阻塞等待, ?直等到之前的線程解鎖之后, 由操作系統喚醒?個新的線程,再來獲取到這個鎖.
注意:
? 上?個線程解鎖之后, 下?個線程并不是?即就能獲取到鎖. ?是要靠操作系統來 “喚醒”. 這也就是操作系統線程調度的?部分?作.
? 假設有 A B C 三個線程, 線程 A 先獲取到鎖, 然后 B 嘗試獲取鎖, 然后 C 再嘗試獲取鎖, 此時 B 和 C都在阻塞隊列中排隊等待。 但是當 A 釋放鎖之后, 雖然 B ? C 先來的, 但是 B 不?定就能獲取到鎖,?是和 C 重新競爭, 并不遵守先來后到的規則.
鎖, 本質上也是操作系統提供的功能,內核提供的功能 =>通過 api 給應用程序了。java (VM)對于這樣的系統 api 又進行了封裝.
synchronized 是調用 系統的 api 進行加鎖。系統 api 本質上是靠 cpu 上的特定指令完成加鎖
(2)可重?
synchronized 加鎖的效果,也可以稱為"互斥性。synchronized 還有一些其他特性:
理解 “把??鎖死”
?個線程沒有釋放鎖, 然后?嘗試再次加鎖.
// 第?次加鎖, 加鎖成功
lock();
// 第?次加鎖, 鎖已經被占?, 阻塞等待.
lock();
按照之前對于鎖的設定, 第?次加鎖的時候, 就會阻塞等待. 直到第?次的鎖被釋放, 才能獲取到第?個鎖. 但是釋放第?個鎖也是由該線程來完成, 結果這個線程已經躺平了, 啥都不想?了, 也就?法進?解鎖操作. 這時候就會 死鎖.
這樣的鎖稱為 不可重?鎖
for (int i = 0; i < 50000; i++) {synchronized (locker) {synchronized (locker) {count++;}}
}
看起來是兩次一樣的加鎖,沒有必要。但是實際上開發中,很容易寫出這樣的代碼的。
一旦方法調用的層次比較深,就搞不好容易出現這樣的情況
要想解除阻塞,需要往下執行才可以,要想往下執行就需要等到第一次的鎖被釋放,這樣的問題,就稱為"死鎖”。
這樣的代碼在 Java 中其實是不會死鎖的!!! 為了避免程序猿粗心大意搞出死鎖!java引入了"可重入機制",Java 中的 synchronized 是 可重?鎖, 因此沒有上?的問題。
最外層“ { ”真正加鎖
最外層“ }” 真正解鎖
站在 JVM 的視角,看到多個}需要執行,JVM 如何知道哪個}是真正解鎖的那個??
先引入一個變量,計數器(0),每次觸發{的時候,把計數器++,每次觸發 } 的時候,把計數器 - -,當計數器 - - 為 0 的時候, 就是真正需要解鎖的時候~
在可重?鎖的內部, 包含了 “線程持有者” 和 “計數器” 兩個信息:(1)如果某個線程加鎖的時候, 發現鎖已經被?占?, 但是恰好占?的正是??, 那么仍然可以繼續獲取到鎖, 并讓計數器?增.(2)解鎖的時候計數器遞減為 0 的時候, 才真正釋放鎖. (才能被別的線程獲取到)
死鎖是面試中考察的重點,也是工作中,多線程開發中非常核心的注意事項~~
若面試官的問題:
如何自己實現一個可重入鎖?
1.在鎖內部記錄當前是哪個線程持有的鎖,后續每次加鎖,都進行判定
2.通過計數器,記錄當前加鎖的次數,從而確定何時真正進行解鎖,
死鎖(死鎖的進一步討論)
“死鎖”是多線程代碼中的一類經典問題, 加鎖是能解決線程安全問題,但是如果加鎖方式不當,就可能產生死鎖!!
死鎖同樣也是經典面試題!!
死鎖的三種典型場景:
場景1. 一個線程, 一把鎖.
剛才說情況 如果鎖是不可重入鎖,并且一個線程針對一個鎖對象,連續加鎖兩次,就會出現死鎖(鑰匙鎖屋里了)
通過引入可重入鎖,問題就迎刃而解了
場景2. 兩個線程,兩把鎖
兩個線程,兩把鎖,每個線程獲取到一把鎖之后,嘗試獲取對方的鎖。線程1 獲取到 鎖A,線程2 獲取到 鎖B,接下來1 嘗試獲取 B,2 嘗試獲取 A,就同樣出現死鎖了!!!(屋鑰匙鎖車里了,車鑰匙鎖屋里了)
如果不加 sleep, 很可能 t1 一口氣就把 locker1 和 locker2 都拿到了.這個時候,t2 還沒開動呢~ 自然無法構成死鎖.
經典面試題:讓你手寫一個出現死鎖的代碼:
C++方向,代碼就好寫,直接加鎖兩次就行了
Java 方向,就得通過上述代碼,兩個線程兩把鎖,精確控制好加鎖的順序
這里也就需要讓我們知道,如果遇到死鎖問題,就可以通過上述調用棧+狀態進行定位了
場景3. N 個線程 M 把鎖
一個經典的模型,哲學家就餐問題(學校的操作系統課上,也會有這個東西)
死鎖,非常嚴重的問題~~ 屬于程序中最嚴重的一類 bug !!!
一旦出現死鎖,線程就"卡住了"無法繼續工作,一個進程中的線程個數,就那么多。更可怕的是,死鎖這種bug, 往往都是概率 出現,測試的時候怎么測試都沒事,一發布就出問題,發布了也沒問題,等到夜深人靜,大家都睡著,突然給你整出點問題!比 bug 更可怕的是,“概率性出現的 bug”。雖然概率小,但是我們也需要重視!! 假設上述問題的 概率是 萬分之一,同樣是需要我們處理的,當時阿里這邊的服務器每天的訪問量是 3億次,每天就有 3萬個用戶,觸發了這個 bug!
如何避免死鎖問題?
教科書上經典的,死鎖的四個必要條件 !!!(下列四個條件,要求大家背下來!!面試經典問題!!)必要條件: 缺一不可!任何一個死鎖的場景,都必須同時具備上述四點,只要缺少一個,都不會構成死鎖。
1.鎖具有互斥特性.
一個線程拿到鎖之后,其他線程就得阻塞等待(鎖最基本的特性.,不太好破壞)
2.鎖不可搶占(不可被剝奪)
一個線程拿到鎖之后,除非他自己主動釋放鎖,否則別人搶不走~~(也是鎖最基本的特性.,也不好破壞)
3.請求和保持
一個線程拿到一把鎖之后,不釋放這個鎖的前提下,再嘗試獲取其他鎖。(如果先放下左手的筷子,再拿右手的筷子, 就不會構成死鎖! 代碼中加鎖的時候,不要去“嵌套”。這種做法, 通用性, 不夠的。 嵌套,很難避免:有些情況下,確實是需要拿到多個鎖, 再進行某個操作的.)
4.循環等待. 多個線程獲取多個鎖的過程中,出現了循環等待。A 等待 B, B 也等待 A 或者 A 等待 B,B 等待 C, C 等待 A。(約定好加鎖的順序(比如按照編號從小到大的順序),就可以破除循環等待了)
解決死鎖問題,核心思路, 破壞上述的必要條件,只要能破壞一個,就搞定!!上述破壞3 4兩種 是開發中比較實用的方法,還有一些其他方案,也能解決死鎖問題.但引入加鎖順序的規則(普適性高, 方案容易落地)
死鎖的小結:
死鎖這里非常重要的,時面試高頻的問題。
"談談你對于死鎖的理解”
死鎖:
1.死鎖是啥
2.死鎖的三個場景
3. 死鎖的危害
4.死鎖的必要條件, 如何解決死鎖
5.2 synchronized 使??例
synchronized 本質上要修改指定對象的 “對象頭”. 從使??度來看, synchronized 也勢必要搭配?個具體的對象來使?.
(1) 修飾代碼塊: 明確指定鎖哪個對象.
鎖任意對象
public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {}}
}
鎖當前對象
public class SynchronizedDemo {public void method() {synchronized (this) {}}
}
(2) 直接修飾普通?法: 鎖的 SynchronizedDemo 對象
public class SynchronizedDemo {public synchronized void methond() {}
}
修飾一個普通方法,就可以省略"鎖對象。
等價于:
(3) 修飾靜態?法: 鎖的 SynchronizedDemo 類的對象
public class SynchronizedDemo {public synchronized static void method() {}
}
synchronized 修飾普通方法, 相當于給 this 加鎖 (鎖對象 this)
synchronized 修飾靜態方法,相當于給類對象加鎖
我們重點要理解,synchronized 鎖的是什么.
兩個線程競爭同?把鎖, 才會產?阻塞等待.
兩個線程分別嘗試獲取兩把不同的鎖, 不會產?競爭.
- 如果我一個線程加鎖,一個線程不加鎖,是否會存在線程安全問題?
就不會出現鎖競爭了!!!會存在線程安全問題 - 如果兩個線程,針對不同的對象加鎖呢?
也會存在線程安全問題
在一個程序中,鎖,不一定只有一把。一個廁所,可能有多個坑位是一樣的。每個坑位都有一個鎖,如果你兩個線程,針對不同的坑位加鎖,不會產生互斥的(也稱為 鎖競爭/鎖沖突)。只有是針對同一個坑位加鎖,才有互斥。
代碼中,可以創建出多個鎖。具體寫代碼的時候,想搞幾個鎖,就搞幾個。只有多個線程競爭同一把鎖,才會產生互斥,針對不同的鎖,則不會。 - 針對加鎖操作的一些混淆的理解
把 count 放到一個 Test.t 對象中. 通過上述 add 方法來進行修改,加鎖的時候鎖對象,寫作 this
synchronized (Test.class){ } 獲取類對象 :
在 java 代碼中就可以通過類名.class 的方式拿到這個類對象。反射 api 就是從上述對象中獲取信息的。
一個 java 進程中, 某個類,只能有唯一一個類對象
synchronized 的變種寫法,可以使用 synchronized 修飾方法 。synchronized (this),也可以等價把 synchronized 加到方法上。
方法中還有一個特殊的情況:
static 修飾的方法,不存在 this.(static 修飾的方法,也叫做"類方法,不是針對"實例"的方法,而是針對類的,在這個方法中, 沒有 this.) 此時, synchronized 修飾 static 方法, 相當于針對類對象加鎖
其他編程語言中,加鎖解鎖, 都是單獨的方法。對比其他語言,java 的加鎖操作風格是獨樹一幟的。Java 中為啥使用 synchronized + 代碼塊 做法?而不是采用 lock + unlock 函數的方式來搭配呢?
像 C++ 這種寫法, 就可能會,忘記調用 unlock(unlock 沒有執行到),如果忘記調用 unlock 其他線程都無法獲取到這個鎖, 產生嚴重的 bug!!
Java 采取的 synchronized, 就能確保, 只要出了 } 一定能釋放鎖. 無論因為 return 還是因為 異常,無論里面調用了哪些其他代碼,都是可以確保 解鎖 操作執行到的.
只要我寫了 lock,就會立即加上 unlock 。這種說法,純純的,大豬蹄子行為,你給妹子保證,我這輩子只愛你一個,永遠不會變心。就算你非常細心,能夠確保每個 條件都加 unlock,但是你不能保證,你們組新來的實習生,也能做到這一點(各位同學們, 你們很可能就是這個實習生)
(其實在 Java 中,也有 lock/unlock 風格的鎖, 一般很少使用)
但是c++沒有 finally ,只能靠程序猿人工來保證了~~(很有可能,java 程序員代碼早早寫完,也沒啥 bug, 下班回去打游戲了,C++ 程序員還在苦苦尋找哪里沒有釋放鎖)。但是更新版本的 C++ 引入了 lock quard (守衛)這個東西,可以起到類似于 synchronized,代碼塊結束之后,就能自動釋放鎖。
5.3 Java 標準庫中的線程安全類
- Java 標準庫中很多都是線程不安全的. 這些類可能會涉及到多線程修改共享數據, ?沒有任何加鎖措施.(把加鎖決策交給程序員)
線程不安全.多個線程,嘗試修改同一個上述的對象,就很容易出現問題!! 而不是 100%,也可能你這個代碼寫出來之后,是沒問題的,具體代碼具體分析(多線程代碼,稍微變換一點,就可能有不一樣的結果)
? ArrayList
? LinkedList
? HashMap
? TreeMap
? HashSet
? TreeSet
? StringBuilder - 但是還有?些是線程安全的. 使?了?些鎖機制來控制.
自帶了鎖, 在多線程環境下時候,能好點。也不是 100% 不出問題!! 只是概率比上面小很多,具體代碼具體分析!!!(多線程代碼,稍微變換一點, 就可能有不一樣的結果)
像Vector,HashTable,StringBuffer 這幾個類都屬于是 標準庫 即將棄用,不推薦使用,暫時還留著(保持和老的代碼兼容)。這個時候,新的代碼就不要用了,未來某一天新版本的 jdk,就把這些內容給刪了。
? Vector (不推薦使?)
? HashTable (不推薦使?)
Java 早起,各位 Java 大佬還不夠成熟時,引入的設定。現在的話這些設定已經被推翻了,不建議使用了.
? ConcurrentHashMap
相比于 HashTable 來說,高度優化的版本(后續詳細分析)
? StringBuffer
StringBuffer 的核??法都帶有 synchronized .
一旦代碼中, 使用了鎖,意味著代碼可能會因為鎖的競爭,產生阻塞=>程序的執行效率大打折扣.
一定要思考清楚, 這個地方是否確食需要鎖,不需要的時候不要亂加.
線程阻塞 =>從 cpu 上調度走,啥時候能調度回來繼續執行???不好說了~~ 滄海桑田 - 還有的雖然沒有加鎖, 但是不涉及 “修改”, 仍然是線程安全的
? String