JAVA笔记:死锁的详细解释
? ? 由多線程帶來的性能改善是以可靠性為代價的,主要是因為有可能產(chǎn)生線程死鎖。死鎖是這樣一種情形:多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞,因此程序不能正常運行。簡單的說就是:線程死鎖時,第一個線程等待第二個線程釋放資源,而同時第二個線程又在等待第一個線程釋放資源。這里舉一個通俗的例子:如在人行道上兩個人迎面相遇,為了給對方讓道,兩人同時向一側(cè)邁出一步,雙方無法通過,又同時向另一側(cè)邁出一步,這樣還是無法通過。假設(shè)這種情況一直持續(xù)下去,這樣就會發(fā)生死鎖現(xiàn)象。?
? ? 導(dǎo)致死鎖的根源在于不適當(dāng)?shù)剡\用“synchronized”關(guān)鍵詞來管理線程對特定對象的訪問。“synchronized”關(guān)鍵詞的作用是,確保在某個時刻只有一個線程被允許執(zhí)行特定的代碼塊,因此,被允許執(zhí)行的線程首先必須擁有對變量或?qū)ο蟮呐潘栽L問權(quán)。當(dāng)線程訪問對象時,線程會給對象加鎖,而這個鎖導(dǎo)致其它也想訪問同一對象的線程被阻塞,直至第一個線程釋放它加在對象上的鎖。?
? ? Java中每個對象都有一把鎖與之對應(yīng)。但Java不提供單獨的lock和unlock操作。下面筆者分析死鎖的兩個過程“上鎖”和“鎖死” 。
上鎖?
? ? 許多線程在執(zhí)行中必須考慮與其他線程之間共享數(shù)據(jù)或協(xié)調(diào)執(zhí)行狀態(tài),就需要同步機(jī)制。因此大多數(shù)應(yīng)用程序要求線程互相通信來同步它們的動作,在 Java 程序中最簡單實現(xiàn)同步的方法就是上鎖。在 Java 編程中,所有的對象都有鎖。線程可以使用 synchronized 關(guān)鍵字來獲得鎖。在任一時刻對于給定的類的實例,方法或同步的代碼塊只能被一個線程執(zhí)行。這是因為代碼在執(zhí)行之前要求獲得對象的鎖。?
? ? 為了防止同時訪問共享資源,線程在使用資源的前后可以給該資源上鎖和開鎖。給共享變量上鎖就使得 Java 線程能夠快速方便地通信和同步。某個線程若給一個對象上了鎖,就可以知道沒有其他線程能夠訪問該對象。即使在搶占式模型中,其他線程也不能夠訪問此對象,直到上鎖的線程被喚醒、完成工作并開鎖。那些試圖訪問一個上鎖對象的線程通常會進(jìn)入睡眠狀態(tài),直到上鎖的線程開鎖。一旦鎖被打開,這些睡眠進(jìn)程就會被喚醒并移到準(zhǔn)備就緒隊列中。?
鎖死?
? ? 如果程序中有幾個競爭資源的并發(fā)線程,那么保證均衡是很重要的。系統(tǒng)均衡是指每個線程在執(zhí)行過程中都能充分訪問有限的資源,系統(tǒng)中沒有餓死和死鎖的線程。當(dāng)多個并發(fā)的線程分別試圖同時占有兩個鎖時,會出現(xiàn)加鎖沖突的情形。如果一個線程占有了另一個線程必需的鎖,互相等待時被阻塞就有可能出現(xiàn)死鎖。?
? ? 在編寫多線程代碼時,筆者認(rèn)為死鎖是最難處理的問題之一。因為死鎖可能在最意想不到的地方發(fā)生,所以查找和修正它既費時又費力。
幾種常見死鎖及對策?
(1)數(shù)據(jù)庫死鎖?
在數(shù)據(jù)庫中,如果一個連接占用了另一個連接所需的數(shù)據(jù)庫鎖,則它可以阻塞另一個連接。如果兩個或兩個以上的連接相互阻塞,則它們都不能繼續(xù)執(zhí)行,這種情況稱為數(shù)據(jù)庫死鎖。?
數(shù)據(jù)庫死鎖問題不易處理,通常數(shù)據(jù)行進(jìn)行更新時,需要鎖定該數(shù)據(jù)行,執(zhí)行更新,然后在提交或回滾封閉事務(wù)時釋放鎖。由于數(shù)據(jù)庫平臺、配置的隔離級以及查詢提示的不同,獲取的鎖可能是細(xì)粒度或粗粒度的,它會阻塞(或不阻塞)其他對同一數(shù)據(jù)行、表或數(shù)據(jù)庫的查詢。基于數(shù)據(jù)庫模式,讀寫操作會要求遍歷或更新多個索引、驗證約束、執(zhí)行觸發(fā)器等。每個要求都會引入更多鎖。此外,其他應(yīng)用程序還可能正在訪問同一數(shù)據(jù)庫模式中的某些對象,并獲取不同應(yīng)用程序所具有的鎖。?
所有這些因素綜合在一起,數(shù)據(jù)庫死鎖幾乎不可能被消除了。值得慶幸的是,數(shù)據(jù)庫死鎖通常是可恢復(fù)的:當(dāng)數(shù)據(jù)庫發(fā)現(xiàn)死鎖時,它會強(qiáng)制銷毀一個連接(通常是使用最少的連接),并回滾其事務(wù)。這將釋放所有與已經(jīng)結(jié)束的事務(wù)相關(guān)聯(lián)的鎖,至少允許其他連接中有一個可以獲取它們正在被阻塞的鎖。?
由于數(shù)據(jù)庫具有這種典型的死鎖處理行為,所以當(dāng)出現(xiàn)數(shù)據(jù)庫死鎖問題時,數(shù)據(jù)庫常常只能重試整個事務(wù)。當(dāng)數(shù)據(jù)庫連接被銷毀時,會拋出可被應(yīng)用程序捕獲的異常,并標(biāo)識為數(shù)據(jù)庫死鎖。如果允許死鎖異常傳播到初始化該事務(wù)的代碼層之外,則該代碼層可以啟動一個新事務(wù)并重做先前所有工作。?
當(dāng)出現(xiàn)問題就重試,由于數(shù)據(jù)庫可以自由地獲取鎖,所以幾乎不可能保證兩個或兩個以上的線程不發(fā)生數(shù)據(jù)庫死鎖。此方法至少能保證在出現(xiàn)某些數(shù)據(jù)庫死鎖情況時,應(yīng)用程序能正常運行。
(2)資源池耗盡死鎖?
客戶端的增加導(dǎo)致資源池耗盡死鎖是由于負(fù)載而造成的,即資源池太小,而每個線程需要的資源超過了池中的可用資源。假設(shè)連接池最多有10個連接,同時有10個對外部并發(fā)調(diào)用。這些線程中每一個都需要一個數(shù)據(jù)庫連接用來清空池。現(xiàn)在,每個線程都執(zhí)行嵌套的調(diào)用。則所有線程都不能繼續(xù),但又都不放棄自己的第一個數(shù)據(jù)庫連接。這樣,10個線程都將被死鎖。?
研究此類死鎖,會發(fā)現(xiàn)線程存儲中有大量等待獲取資源的線程,以及同等數(shù)量的空閑且未阻塞的活動數(shù)據(jù)庫連接。當(dāng)應(yīng)用程序死鎖時,如果可以在運行時檢測連接池,就能確認(rèn)連接池實際上已空。
修復(fù)此類死鎖的方法包括:增加連接池的大小或者重構(gòu)代碼,以便單個線程不需要同時使用很多數(shù)據(jù)庫連接。或者可以設(shè)置內(nèi)部調(diào)用使用不同的連接池,即使外部調(diào)用的連接池為空,內(nèi)部調(diào)用也能使用自己的連接池繼續(xù)。?
(3)單線程、多沖突數(shù)據(jù)庫連接死鎖?
對同一線程執(zhí)行嵌套的調(diào)用有時出現(xiàn)死鎖,此情形即使在非高負(fù)載系統(tǒng)中通常也會發(fā)生。當(dāng)?shù)谝粋€(外部)連接已獲取第二個(內(nèi)部)連接所需要的數(shù)據(jù)庫鎖,則第二個連接將永久阻塞第一個連接,并等待第一個連接被提交或回滾,這就出現(xiàn)了死鎖情形。因為數(shù)據(jù)庫沒有注意到兩個連接之間的關(guān)系,所以數(shù)據(jù)庫不會將此情形檢測為死鎖。這樣即使不存在并發(fā),此代碼也將導(dǎo)致死鎖。此情形有多種具體的變種,可以涉及多個線程和兩個以上的數(shù)據(jù)庫連接。?
(4)Java虛擬機(jī)鎖與數(shù)據(jù)庫鎖沖突?
這種情形發(fā)生在數(shù)據(jù)庫鎖與Java虛擬機(jī)鎖并存的時候。在這種情況下,一個線程占有一個數(shù)據(jù)庫鎖并嘗試獲取Java虛擬機(jī)鎖。同時,另一個線程占有Java虛擬機(jī)鎖并嘗試獲取數(shù)據(jù)庫鎖。此時,數(shù)據(jù)庫發(fā)現(xiàn)一個連接阻塞了另一個連接,但由于無法阻止連接繼續(xù),所以不會檢測到死鎖。Java虛擬機(jī)發(fā)現(xiàn)同步的鎖中有一個線程,并有另一個嘗試進(jìn)入的線程,所以即使Java虛擬機(jī)能檢測到死鎖并對它們進(jìn)行處理,它還是不會檢測到這種情況。?
死鎖的經(jīng)驗法則
(1) 對大多數(shù)的Java程序員來說最簡單的防止死鎖的方法是對競爭的資源引入序號,如果一個線程需要幾個資源,那么它必須先得到小序號的資源,再申請大序號的資源。可以在Java代碼中增加同步關(guān)鍵字的使用,這樣可以減少死鎖,但這樣做也會影響性能。如果負(fù)載過重,數(shù)據(jù)庫內(nèi)部也有可能發(fā)生死鎖。?
? ? (2)了解數(shù)據(jù)庫鎖的發(fā)生行為。假定任何數(shù)據(jù)庫訪問都有可能陷入數(shù)據(jù)庫死鎖狀況,但是都能正確進(jìn)行重試。例如了解如何從應(yīng)用服務(wù)器獲取完整的線程轉(zhuǎn)儲以及從數(shù)據(jù)庫獲取數(shù)據(jù)庫連接列表(包括互相阻塞的連接),知道每個數(shù)據(jù)庫連接與哪個Java線程相關(guān)聯(lián)。了解Java線程和數(shù)據(jù)庫連接之間映射的最簡單方法是向連接池訪問模式添加日志記錄功能。?
? ? (3)當(dāng)進(jìn)行嵌套的調(diào)用時,了解哪些調(diào)用使用了與其它調(diào)用同樣的數(shù)據(jù)庫連接。即使嵌套調(diào)用運行在同一個全局事務(wù)中,它仍將使用不同的數(shù)據(jù)庫連接,而不會導(dǎo)致嵌套死鎖。?
? ? (4)確保在峰值并發(fā)時有足夠大的資源池。?
? ? (5)避免執(zhí)行數(shù)據(jù)庫調(diào)用或在占有Java虛擬機(jī)鎖時,執(zhí)行其他與Java虛擬機(jī)無關(guān)的操作。?
轉(zhuǎn)載于:https://www.cnblogs.com/tryitboy/p/4231121.html
總結(jié)
以上是生活随笔為你收集整理的JAVA笔记:死锁的详细解释的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 马嵬其二教学反思
- 下一篇: 【BZOJ】【3856】Monster