摘要:例如,張三同時申請賬本和,賬本管理員如果發(fā)現(xiàn)文件架上只有賬本,這個時候賬本管理員是不會把賬本拿下來給張三的,只有賬本和都在的時候才會給張三。但仍需注意的是,有時候預(yù)防死鎖成本也是很高的。
在上一篇中,我們嘗試使用了 Account.class作為互斥鎖,來解決轉(zhuǎn)賬問題。但是很容易發(fā)現(xiàn)這樣,所有的轉(zhuǎn)賬操作都是串行的,性能太差了。
讓我們嘗試提升下性能。
向現(xiàn)實世界要答案現(xiàn)實世界中,轉(zhuǎn)賬操作是支持并發(fā)的。
我設(shè)想下,在古代沒有信息化的時候。賬戶的存在就是一個個賬本,而且每個用戶都有一個賬本。這些賬本都放在架子上。銀行柜員在轉(zhuǎn)賬時候,是去架子上同時拿到轉(zhuǎn)入賬本和轉(zhuǎn)出賬本,然后做轉(zhuǎn)賬都。這時候這個柜員會遇到3種情況
1,架子上剛好有 轉(zhuǎn)入和轉(zhuǎn)出賬本,同時拿走即可。
2,如果架子上只有轉(zhuǎn)入和轉(zhuǎn)出賬本之一,柜員先拿走一本,在等著另一本被送回來。
3,轉(zhuǎn)入和轉(zhuǎn)出賬本都沒有,柜員只好等著2個賬本被送回來。
上面的步驟轉(zhuǎn)換成編碼,其實就是2把鎖實現(xiàn)。轉(zhuǎn)入賬本一把鎖,轉(zhuǎn)出賬本一把鎖。在 transfer() 方法內(nèi)部,我們首先嘗試鎖定轉(zhuǎn)出賬戶 this(先把轉(zhuǎn)出賬本拿到手),然后嘗試鎖定轉(zhuǎn)入賬戶 target(再把轉(zhuǎn)入賬本拿到手),只有當(dāng)兩者都成功時,才執(zhí)行轉(zhuǎn)賬操作。這個邏輯可以圖形化為下圖這個樣子。
如下所示。經(jīng)過這樣的優(yōu)化后,賬戶 A 轉(zhuǎn)賬戶 B 和賬戶 C 轉(zhuǎn)賬戶 D 這兩個轉(zhuǎn)賬操作就可以并行了。
class Account { private int balance; // 轉(zhuǎn)賬 void transfer(Account target, int amt){ // 鎖定轉(zhuǎn)出賬戶 synchronized(this) { // 鎖定轉(zhuǎn)入賬戶 synchronized(target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
相對于用 Account.class 作為互斥鎖,鎖定的范圍太大,而我們鎖定兩個賬戶范圍就小多了,這樣的鎖,上一章我們介紹過,叫細(xì)粒度鎖使用細(xì)粒度鎖可以提高并行度,是性能優(yōu)化的一個重要手段。
使用細(xì)粒度鎖這么簡單嘛?編寫并發(fā)程序就需要這樣時時刻刻保持謹(jǐn)慎。
使用細(xì)粒度鎖是有代價的,這個代價就是可能會導(dǎo)致死鎖。
我們還是通過現(xiàn)實世界看一下死鎖產(chǎn)生的原因。如果有客戶找柜員張三做個轉(zhuǎn)賬業(yè)務(wù):賬戶 A 轉(zhuǎn)賬戶 B 100 元,此時另一個客戶找柜員李四也做個轉(zhuǎn)賬業(yè)務(wù):賬戶 B 轉(zhuǎn)賬戶 A 100 元,于是張三和李四同時都去文件架上拿賬本,這時候有可能湊巧張三拿到了賬本 A,李四拿到了賬本 B。張三拿到賬本 A 后就等著賬本 B(賬本 B 已經(jīng)被李四拿走),而李四拿到賬本 B 后就等著賬本 A(賬本 A 已經(jīng)被張三拿走),他們要等多久呢?他們會永遠(yuǎn)等待下去…因為張三不會把賬本 A 送回去,李四也不會把賬本 B 送回去。我們姑且稱為死等吧。
現(xiàn)實世界里的死等,就是編程領(lǐng)域的死鎖了。
死鎖 一組互相競爭資源的線程因互相等待,導(dǎo)致“永久”阻塞的現(xiàn)象
class Account { private int balance; // 轉(zhuǎn)賬 void transfer(Account target, int amt){ // 鎖定轉(zhuǎn)出賬戶 synchronized(this){ ① // 鎖定轉(zhuǎn)入賬戶 synchronized(target){ ② if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
關(guān)于這種現(xiàn)象,我們還可以借助資源分配圖來可視化鎖的占用情況(資源分配圖是個有向圖,它可以描述資源和線程的狀態(tài))。其中,資源用方形節(jié)點表示,線程用圓形節(jié)點表示;資源中的點指向線程的邊表示線程已經(jīng)獲得該資源,線程指向資源的邊則表示線程請求資源,但尚未得到。轉(zhuǎn)賬發(fā)生死鎖時的資源分配圖就如下圖所示。
轉(zhuǎn)賬發(fā)生死鎖時的資源分配圖
并發(fā)程序一旦死鎖,一般沒有特別好的方法,很多時候我們只能重啟應(yīng)用。因此,解決死鎖問題最好的辦法還是規(guī)避死鎖。
那如何避免死鎖呢?要避免死鎖就需要分析死鎖發(fā)生的條件,有個叫 Coffman 的牛人早就總結(jié)過了,只有以下這四個條件都發(fā)生時才會出現(xiàn)死鎖:
1,互斥,共享資源 X 和 Y 只能被一個線程占用;
2,占有且等待,線程 T1 已經(jīng)取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
3,不可搶占,其他線程不能強(qiáng)行搶占線程 T1 占有的資源;
4,循環(huán)等待,線程 T1 等待線程 T2 占有的資源,線程 T2 等待線程 T1 占有的資源,就是循環(huán)等待。
反過來分析,也就是說只要我們破壞其中一個,就可以成功避免死鎖的發(fā)生
其中,互斥這個條件我們沒有辦法破壞,因為我們用鎖為的就是互斥。不過其他三個條件都是有辦法破壞掉的,到底如何做呢?
1,對于“占用且等待”這個條件,我們可以一次性申請所有的資源,這樣就不存在等待了。
2,對于“不可搶占”這個條件,占用部分資源的線程進(jìn)一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源,這樣不可搶占這個條件就破壞掉了。
3,對于“循環(huán)等待”這個條件,可以靠按序申請資源來預(yù)防。所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化后自然就不存在循環(huán)了。
我們已經(jīng)從理論上解決了如何預(yù)防死鎖,下面我們就來嘗試用代碼實踐一下這些理論。
1. 破壞占用且等待條件從理論上講,要破壞這個條件,可以一次性申請所有資源。在現(xiàn)實世界里,就拿前面我們提到的轉(zhuǎn)賬操作來講。可以增加一個賬本管理員,然后只允許賬本管理員從文件架上拿賬本,也就是說柜員不能直接在文件架上拿賬本,必須通過賬本管理員才能拿到想要的賬本。例如,張三同時申請賬本 A 和 B,賬本管理員如果發(fā)現(xiàn)文件架上只有賬本 A,這個時候賬本管理員是不會把賬本 A 拿下來給張三的,只有賬本 A 和 B 都在的時候才會給張三。這樣就保證了“一次性申請所有資源”。
通過賬本管理員拿賬本圖
對應(yīng)到編程領(lǐng)域,“同時申請”這個操作是一個臨界區(qū),我們也需要一個角色(Java 里面的類)來管理這個臨界區(qū),我們就把這個角色定為 Allocator。它有兩個重要功能,分別是:同時申請資源 apply() 和同時釋放資源 free()。賬戶 Account 類里面持有一個 Allocator 的單例(必須是單例,只能由一個人來分配資源)。當(dāng)賬戶 Account 在執(zhí)行轉(zhuǎn)賬操作的時候,首先向 Allocator 同時申請轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶這兩個資源,成功后再鎖定這兩個資源;當(dāng)轉(zhuǎn)賬操作執(zhí)行完,釋放鎖之后,我們需通知 Allocator 同時釋放轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶這兩個資源。具體的代碼實現(xiàn)如下。
class Allocator { private List2. 破壞不可搶占條件
破壞不可搶占條件看上去很簡單,核心是要能夠主動釋放它占有的資源,這一點 synchronized 是做不到的。原因是 synchronized 申請資源的時候,如果申請不到,線程直接進(jìn)入阻塞狀態(tài)了,而線程進(jìn)入阻塞狀態(tài),也釋放不了線程已經(jīng)占有的資源。java.util.concurrent 這個包下面提供的 Lock 是可以輕松解決這個問題的。關(guān)于這個話題,咱們后面會詳細(xì)講。
3. 破壞循環(huán)等待條件破壞這個條件,需要對資源進(jìn)行排序,然后按序申請資源。這個實現(xiàn)非常簡單,我們假設(shè)每個賬戶都有不同的屬性 id,這個 id 可以作為排序字段,申請的時候,我們可以按照從小到大的順序來申請。比如下面代碼中,①~⑥處的代碼對轉(zhuǎn)出賬戶(this)和轉(zhuǎn)入賬戶(target)排序,然后按照序號從小到大的順序鎖定賬戶。這樣就不存在“循環(huán)”等待了。
class Account { private int id; private int balance; // 轉(zhuǎn)賬 void transfer(Account target, int amt){ Account left = this ① Account right = target; ② if (this.id > target.id) { ③ left = target; ④ right = this; ⑤ } ⑥ // 鎖定序號小的賬戶 synchronized(left){ // 鎖定序號大的賬戶 synchronized(right){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } }總結(jié)
當(dāng)我們在編程世界里遇到問題時,應(yīng)不局限于當(dāng)下,可以換個思路,向現(xiàn)實世界要答案,利用現(xiàn)實世界的模型來構(gòu)思解決方案,這樣往往能夠讓我們的方案更容易理解,也更能夠看清楚問題的本質(zhì)。
用細(xì)粒度鎖來鎖定多個資源時,要注意死鎖的問題.
預(yù)防死鎖主要是破壞三個條件中的一個,有了這個思路后,實現(xiàn)就簡單了。但仍需注意的是,有時候預(yù)防死鎖成本也是很高的。例如上面轉(zhuǎn)賬那個例子,我們破壞占用且等待條件上我們也是鎖了所有的賬戶,而且還是用了死循環(huán) while(!actr.apply(this, target));方法,不過好在 apply() 這個方法基本不耗時。 在轉(zhuǎn)賬這個例子中,破壞循環(huán)等待條件就是成本最低的一個方案。
所以我們在選擇具體方案的時候,還需要評估一下操作成本,從中選擇一個成本最低的方案
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/74155.html
摘要:如何檢測死鎖由于死鎖極難通過人工的方式查出來,因此提供了命令來檢測某個進(jìn)程中心線程的情況,并排查有沒有死鎖。線程持有的鎖,等待的鎖。避免出現(xiàn)死鎖,如果出現(xiàn)了死鎖,則可以使用命令查看線程是否有死鎖。 showImg(https://segmentfault.com/img/remote/1460000014936757); 前言 在 Java 的并發(fā)編程中,有一個問題需要特別注意,那就是...
摘要:因為多線程競爭鎖時會引起上下文切換。減少線程的使用。舉個例子如果說服務(wù)器的帶寬只有,某個資源的下載速度是,系統(tǒng)啟動個線程下載該資源并不會導(dǎo)致下載速度編程,所以在并發(fā)編程時,需要考慮這些資源的限制。 最近私下做一項目,一bug幾日未解決,總惶恐。一日頓悟,bug不可怕,怕的是項目不存在bug,與其懼怕,何不與其剛正面。 系列文章傳送門: Java多線程學(xué)習(xí)(一)Java多線程入門 Jav...
摘要:相比與其他操作系統(tǒng)包括其他類系統(tǒng)有很多的優(yōu)點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。因為多線程競爭鎖時會引起上下文切換。減少線程的使用。很多編程語言中都有協(xié)程。所以如何避免死鎖的產(chǎn)生,在我們使用并發(fā)編程時至關(guān)重要。 系列文章傳送門: Java多線程學(xué)習(xí)(一)Java多線程入門 Java多線程學(xué)習(xí)(二)synchronized關(guān)鍵字(1) java多線程學(xué)習(xí)(二)syn...
摘要:在這個范圍廣大的并發(fā)技術(shù)領(lǐng)域當(dāng)中多線程編程可以說是基礎(chǔ)和核心,大多數(shù)抽象并發(fā)問題的構(gòu)思與解決都是基于多線程模型來進(jìn)行的。一般來說,多線程程序會面臨三類問題正確性問題效率問題死鎖問題。 多線程編程或者說范圍更大的并發(fā)編程是一種非常復(fù)雜且容易出錯的編程方式,但是我們?yōu)槭裁催€要冒著風(fēng)險艱辛地學(xué)習(xí)各種多線程編程技術(shù)、解決各種并發(fā)問題呢? 因為并發(fā)是整個分布式集群的基礎(chǔ),通過分布式集群不僅可以大...
摘要:上一章介紹過關(guān)鍵字,使用它可以給程序互斥部分加上一把鎖從而達(dá)到同步的效果,但錯誤的用法會導(dǎo)致多個線程同時被阻塞死鎖死鎖多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不可能正常終止。 上一章介紹過synchronized關(guān)鍵字,使用它可以給程序互斥部分加上一把鎖從而達(dá)到同步的效果,但錯誤的用法會導(dǎo)致多個線程同時被阻塞.... 死鎖 死鎖...
閱讀 1262·2021-11-23 09:51
閱讀 2637·2021-09-03 10:47
閱讀 2233·2019-08-30 15:53
閱讀 2413·2019-08-30 15:44
閱讀 1374·2019-08-30 15:44
閱讀 1193·2019-08-30 10:57
閱讀 1923·2019-08-29 12:25
閱讀 1086·2019-08-26 11:57