synchronized修飾方法
synchronized可以修飾代碼塊(在線程入門2中有例子),也可以修飾普通方法和靜態方法。
修飾普通方法
修飾普通方法簡化寫法:
修飾靜態方法
修飾靜態方法簡化寫法:
注意:利用synchronized上鎖,鎖的對象是什么不重要,重要的的是兩個線程中鎖的對象是否是同一個對象。
synchronized 的可重入性
所謂的可重入性,指的是,一個線程,連續對一把鎖,加鎖兩次,或者更多次,不會出現死鎖,滿足這個條件就叫做“可重入性”。
例如下面代碼
假設在我們第一次加鎖時成功,那么locker就屬于鎖定狀態,緊接著往下繼續執行代碼,又發現需要對locker進行上鎖,但是剛才已經上過鎖了,正常來說,對已經在一個線程中上過鎖的對象,在另一個線程中又要對相同的對象進行上鎖,就會發生“阻塞等待”,需要這個對象被解鎖釋放后才能進行上鎖。
那么對于上面代碼這種情況就會產生“死鎖”,導致線程卡死,第二個想要加鎖成功就需要第一次釋放鎖,需要執行到程序“1號”位置,但是想要到“1號”位置 就需要 第二次能成功加鎖讓程序繼續往下走,由于第二次加鎖處在“阻塞等待“狀態,也就執不了代碼,最終到不了2號位置,也就無法釋放鎖,因此線程就直接被卡死了。
因此在Java中synchronized被設計成”可重入鎖“,就能夠有效解決問題。但C++中std::mutex就是不可重入鎖,就會出現死鎖。
還需注意:當出現這種重復 上鎖的情況時,在釋放鎖時,無論多少層都必須要在最外層釋放鎖。
關于死鎖
死鎖的案例
1.一個線程,針對一把鎖,連續加鎖兩次及以上,如果是不可重入鎖,就會出現死鎖。
2.兩個線程,兩把鎖。此時無論是不是可重入鎖都回死鎖
上面這種情況就類似于生活中,家門鑰匙被鎖在車里了,車鑰匙被鎖在加里了。
3.N個線程,M把鎖 (此時更容易出現死鎖)
一個經典的模型就是”哲學家問題“
如圖所示:
有五個哲學家,桌子上有五根筷子,哲學家只做兩件事,一個是停下來思考人生什么都不做,另一個是拿起左邊和右邊的筷子吃面條。
哲學家什么時候吃面條,什么時候吃完停下來思考人生都是隨機的,如果其中一個哲學家拿起左右兩邊的筷子吃面條,此時相鄰的哲學家就只能等待他吃完放下筷子,就會出現阻塞等待。
本來正常運轉都沒什么問題,但是有一天突然出現,每個哲學家都先拿起左手邊的筷子,結果發現右手邊的筷子已經被拿了,此時哲學家也不會放下自己左邊的筷子,只會進行等待右邊何時放下筷子,這個時候沒人能吃上面條,也沒人能放下筷子,就導致了出現了死鎖。
死鎖是比較嚴重的bug(會導致線程卡住,也就無法在繼續執行后序工作了)
死鎖涉及到的四個必要條件
1.互斥使用 (鎖的基本特征)當一個線程持有一把鎖后,另一個線程想獲得鎖,就要進行阻塞等待。
2.不可搶占 (鎖的基本特征)當鎖已經被線程1拿到之后,線程2只能等待線程1主動釋放,不嫩惡搞強行搶過來。
3.請求保持 (代碼結構) 一個線程嘗試獲取多把鎖 (先拿到鎖1,嘗試獲取鎖2,獲取的時候,鎖1不會釋放,嵌套結構)
4.循環等待/環路等待 (代碼結構)等待的依賴關系形成環了(哲學家問題)
解決死鎖
解決死鎖的核心就是破壞上述的必要條件,只要破壞一個,死鎖就形成了,但一和二是synchronized的自帶的特性,無法干預,只能從3,4入手。
對于3來說,我們可以調整代碼結構,把嵌套鎖的結構,改成并列結構。
對于4來說,可以約定加鎖的順序,就可以避免循環等待 (例如針對鎖進行編號,加多把鎖的時候,先加編號小的鎖,再加編號大的鎖,所有線程都要遵循這個規則)