天天看點

面試必問并發安全問題

作者:悠閑觀自在

線程安全性

什麼是線程安全性?我們可以這麼了解,我們所寫的代碼在并發情況下使用時,總是能表現出正确的行為;反之,未實作線程安全的代碼,表現的行為是不可預知的,有可能正确,而絕大多數的情況下是錯誤的。正如Java語言規範在《Chapter 17. Threads and Locks》所說的:

面試必問并發安全問題

圖中标紅文字的意思是:線程的行為(尤其是在未正确同步的情況下)可能會造成混淆并且違反直覺。本章描述了多線程程式的語義。它包括規則,通過讀取多個線程更新的共享記憶體可以看到值。

如果要實作線程安全性,就要保證我們的類是線程安全的的。在《Java并發程式設計實戰》中,定義“類是線程安全的”如下:

當多個線程通路某個類時,不管運作時環境采用何種排程方式或者這些線程将如何交替執行,并且在調用代碼中不需要任何額外的同步或者協同,這個類都能表現出正确的行為,那麼就稱這個類是線程安全的。

如何實作呢?

線程封閉

實作好的并發是一件困難的事情,是以很多時候我們都想躲避并發。避免并發最簡單的方法就是線程封閉。什麼是線程封閉呢?

就是把對象封裝到一個線程裡,隻有這一個線程能看到此對象。那麼這個對象就算不是線程安全的也不會出現任何安全問題。

棧封閉

棧封閉是我們程式設計當中遇到的最多的線程封閉。什麼是棧封閉呢?簡單的說就是局部變量。多個線程通路一個方法,此方法中的局部變量都會被拷貝一份到線程棧中。是以局部變量是不被多個線程所共享的,也就不會出現并發問題。是以能用局部變量就别用全局的變量,全局變量容易引起并發問題。

TheadLocal

ThreadLocal是實作線程封閉的最好方法。ThreadLocal内部維護了一個Map,Map的key是每個線程的名稱,而Map的值就是我們要封閉的對象。每個線程中的對象都對應着Map中一個值,也就是ThreadLocal利用Map實作了對象的線程封閉。

無狀态的類

沒有任何成員變量的類,就叫無狀态的類,這種類一定是線程安全的。

如果這個類的方法參數中使用了對象,也是線程安全的嗎?比如:

面試必問并發安全問題

當然也是,為何?因為多線程下的使用,固然user這個對象的執行個體會不正常,但是對于StatelessClass這個類的對象執行個體來說,它并不持有UserVo的對象執行個體,它自己并不會有問題,有問題的是UserVo這個類,而非StatelessClass本身。

讓類不可變

讓狀态不可變,加final關鍵字,對于一個類,所有的成員變量應該是私有的,同樣的隻要有可能,所有的成員變量應該加上final關鍵字,但是加上final,要注意如果成員變量又是一個對象時,這個對象所對應的類也要是不可變,才能保證整個類是不可變的。

但是要注意,一旦類的成員變量中有對象,上述的final關鍵字保證不可變并不能保證類的安全性,為何?因為在多線程下,雖然對象的引用不可變,但是對象在堆上的執行個體是有可能被多個線程同時修改的,沒有正确處理的情況下,對象執行個體在堆中的資料是不可預知的。

面試必問并發安全問題

加鎖和CAS

我們最常使用的保證線程安全的手段,使用synchronized關鍵字,使用顯式鎖,使用各種原子變量,修改資料時使用CAS機制等等。

死鎖

概念

是指兩個或兩個以上的程序在執行過程中,由于競争資源或者由于彼此通信而造成的一種阻塞的現象,若無外力作用,它們都将無法推進下去。此時稱系統處于死鎖狀态或系統産生了死鎖。

舉個例子:A和B去按摩洗腳,都想在洗腳的時候,同時順便做個頭部按摩,13技師擅長足底按摩,14擅長頭部按摩。

這個時候A先搶到14,B先搶到13,兩個人都想同時洗腳和頭部按摩,于是就互不相讓,揚言我死也不讓你,這樣的話,A搶到14,想要13,B搶到13,想要14,在這個想同時洗腳和頭部按摩的事情上A和B就産生了死鎖。怎麼解決這個問題呢?

第一種,假如這個時候,來了個15,剛好也是擅長頭部按摩的,A又沒有兩個腦袋,自然就歸了B,于是B就美滋滋的洗腳和做頭部按摩,剩下A在旁邊氣鼓鼓的,這個時候死鎖這種情況就被打破了,不存在了。

第二種,C出場了,用武力強迫A和B,必須先做洗腳,再頭部按摩,這種情況下,A和B誰先搶到13,誰就可以進行下去,另外一個沒搶到的,就等着,這種情況下,也不會産生死鎖。

是以總結一下:

1、死鎖是必然發生在多操作者(M>=2個)争奪多個資源(N>=2個,且N<=M)才會發生這種情況。很明顯,單線程自然不會有死鎖,隻有B一個去,不要2個,打十個都沒問題;單資源呢?隻有13,A和B也隻會産生激烈競争,打得不可開交,誰搶到就是誰的,但不會産生死鎖。

2、争奪資源的順序不對,如果争奪資源的順序是一樣的,也不會産生死鎖;

3、争奪者對拿到的資源不放手。

學術化的定義

死鎖的發生必須具備以下四個必要條件。

1)互斥條件:指程序對所配置設定到的資源進行排它性使用,即在一段時間内某資源隻由一個程序占用。如果此時還有其它程序請求資源,則請求者隻能等待,直至占有資源的程序用畢釋放。

2)請求和保持條件:指程序已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它程序占有,此時請求程序阻塞,但又對自己已獲得的其它資源保持不放。

3)不剝奪條件:指程序已獲得的資源,在未使用完之前,不能被剝奪,隻能在使用完時由自己釋放。

4)環路等待條件:指在發生死鎖時,必然存在一個程序——資源的環形鍊,即程序集合{P0,P1,P2,···,Pn}中的P0正在等待一個P1占用的資源;P1正在等待P2占用的資源,……,Pn正在等待已被P0占用的資源。

了解了死鎖的原因,尤其是産生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。

隻要打破四個必要條件之一就能有效預防死鎖的發生。

打破互斥條件:改造獨占性資源為虛拟資源,大部分資源已無法改造。

打破不可搶占條件:當一程序占有一獨占性資源後又申請一獨占性資源而無法滿足,則退出原占有的資源。

打破占有且申請條件:采用資源預先配置設定政策,即程序運作前申請全部資源,滿足則運作,不然就等待,這樣就不會占有且申請。

打破循環等待條件:實作資源有序配置設定政策,對所有裝置實作分類編号,所有程序隻能采用按序号遞增的形式申請資源。

避免死鎖常見的算法有有序資源配置設定法、銀行家算法。

現象、危害和解決

在我們IT世界有沒有存在死鎖的情況,有:資料庫裡多事務而且要同時操作多個表的情況下。是以資料庫設計的時候就考慮到了檢測死鎖和從死鎖中恢複的機制。比如oracle提供了檢測和處理死鎖的語句,而mysql也提供了“循環依賴檢測的機制”

面試必問并發安全問題
面試必問并發安全問題

在Java世界裡存在着多線程争奪多個資源,不可避免的存在着死鎖。那麼我們在編寫代碼的時候什麼情況下會發生呢?

現象

簡單順序死鎖

動态順序死鎖

顧名思義也是和擷取鎖的順序有關,但是比較隐蔽,不像簡單順序死鎖,往往從代碼一眼就看出擷取鎖的順序不對。

危害

1、線程不工作了,但是整個程式還是活着的

2、沒有任何的異常資訊可以供我們檢查。

3、一旦程式發生了發生了死鎖,是沒有任何的辦法恢複的,隻能重新開機程式,對生産平台的程式來說,這是個很嚴重的問題。

實際工作中的死鎖

時間不定,不是每次必現;一旦出現沒有任何異常資訊,隻知道這個應用的所有業務越來越慢,最後停止服務,無法确定是哪個具體業務導緻的問題;測試部門也無法複現,并發量不夠。

解決

定位

要解決死鎖,當然要先找到死鎖,怎麼找?

通過jps 查詢應用的 id,再通過jstack id 檢視應用的鎖的持有情況

面試必問并發安全問題
面試必問并發安全問題
面試必問并發安全問題

修正

關鍵是保證拿鎖的順序一緻

兩種解決方式

    • 内部通過順序比較,确定拿鎖的順序;
    • 采用嘗試拿鎖的機制。

其他安全問題

活鎖

兩個線程在嘗試拿鎖的機制中,發生多個線程之間互相謙讓,不斷發生同一個線程總是拿到同一把鎖,在嘗試拿另一把鎖時因為拿不到,而将本來已經持有的鎖釋放的過程。

解決辦法:每個線程休眠随機數,錯開拿鎖的時間。

線程饑餓

低優先級的線程,總是拿不到執行時間

線程安全的單例模式

在設計模式中,單例模式是比較常見的一種設計模式,如何實作單例呢?一種比較常見的是雙重檢查鎖定。

雙重檢查鎖定

面試必問并發安全問題

上面的雙重檢查鎖定卻存在着線程安全問題,為什麼呢?這是因為

singleDcl = new SingleDcl();

雖然隻有一行代碼,但是其實在具體執行的時候有好幾步操作:

1、JVM為SingleDcl的對象執行個體在記憶體中配置設定空間

2、進行對象初始化,完成new操作

3、JVM把這個空間的位址賦給我們的引用singleDcl

因為JVM内部的實作原理(指并發相關的重排序等,後面的課程會學到),會産生一種情況,第3步會在第2步之前執行。

于是在多線程下就會産生問題:A線程正在syn同步塊中執行singleDcl = new SingleDcl(),此時B線程也來執行getInstance(),進行了singleDcl == null的檢查,因為第3步會在第2步之前執行,B線程檢查發現singleDcl不為null,會直接拿着singleDcl執行個體使用,但是這時A線程還在執行對象初始化,這就導緻B線程拿到的singleDcl執行個體可能隻初始化了一半,B線程通路singleDcl執行個體中的對象域就很有可能出錯。

怎麼解決這個問題呢?在前面聲明singleDcl的位置:

private static SingleDcl singleDcl;

加上volatile關鍵字,變成private volatile static SingleDcl singleDcl; 即可。

單例模式推薦實作

懶漢式

類初始化模式,也叫延遲占位模式。在單例類的内部由一個私有靜态内部類來持有這個單例類的執行個體。因為在JVM中,對類的加載和類初始化,由虛拟機保證線程安全。

面試必問并發安全問題

延遲占位模式還可以用在多線程下執行個體域的延遲指派。

餓漢式

在聲明的時候就new這個類的執行個體,或者使用枚舉也可以。

面試必問并發安全問題

繼續閱讀