溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

java多線程中易犯的錯誤是什么

發(fā)布時間:2022-01-06 15:23:05 來源:億速云 閱讀:109 作者:iii 欄目:大數(shù)據(jù)

本篇內(nèi)容介紹了“java多線程中易犯的錯誤是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

一、什么時候應(yīng)該使用多線程?

不知道大家有沒有想過這個問題,就是什么時候我該使用多線程呢?使用多線程就一定會提升系統(tǒng)性能嗎?

1、其實是否應(yīng)該使用多線程在很大程度上取決于應(yīng)用程序的類型。

  • 計算密集型(如純數(shù)學(xué)運算) 的, 并受CPU 功能的制約, 則只有多CPU(或者多個內(nèi)核) 機器能夠從更多的線程中受益, 單CPU下, 多線程不會帶來任何性能上的提升, 反而有可能由于線程切換等額外開銷而導(dǎo)致性能下降

  • IO密集型的,當(dāng)我的應(yīng)用必須等待緩慢的資源(如網(wǎng)絡(luò)連接或者數(shù)據(jù)庫連接上返回數(shù)據(jù))時,那么多線程會讓系統(tǒng)的CPU充分的利用起來,當(dāng)一個線程阻塞掛起時,另一個線程可以繼續(xù)使用CPU資源。

  • 其實,就是多線程不會增加CPU的處理能,而是能夠更加充分地利用CPU資源。

由于同一進(jìn)程的多個線程是共享同一片內(nèi)存資源的,在帶來方便的同時也必然會增加其復(fù)雜性,如何保證多線程訪問數(shù)據(jù)的一致性問題等。而多線程屬于編程中容易翻車的地方。并且多線程編程問題的測試定位也是比較難的??傮w來說,好的多線程是寫出來,將多線程問題寄希望于測試中發(fā)現(xiàn), 無疑是極度不可靠的。SO,努力的學(xué)習(xí)吧。

Java API 與多線程息息相關(guān)的的幾大關(guān)鍵字:volatile、synchronized、 wait、 notify. 理解了這幾個關(guān)鍵字,就可以編寫多線程的代碼了。

二、什么時候需要加鎖?

在多線程場合下,最重要的就是保障數(shù)據(jù)的一致性問題,而保障數(shù)據(jù)一致性問題,就需要借助于鎖了。

其實我們在多線程的場景下應(yīng)該搞清楚一個問題,就是到底什么需要保護(hù)?并不是所有的的數(shù)據(jù)都需要加鎖保護(hù),只有那些涉及到被多線程訪問的共享的數(shù)據(jù)才需要加鎖保護(hù)。

鎖的本質(zhì)其實就是確保在同一時刻,只有一個線程在訪問共享數(shù)據(jù),那么此時該共享數(shù)據(jù)就能得到有效的保護(hù)。

舉例說明下,比如我們想構(gòu)造一個多線程下安全的單向鏈表:

<img src="/Users/luqiang/Downloads/公眾號圖片/鏈表插入新圖.jpg" alt="鏈表插入新圖"  />

假如現(xiàn)在有兩個線程在操作這個鏈表,一個寫線程插入一個新元素7,另一個讀線程遍歷鏈表數(shù)據(jù),如果不使用任何鎖,那就有可能出現(xiàn)下面的執(zhí)行順序:

步驟寫線程讀線程
0修改鏈表元素2的next指針指向7這個元素... ...
1... ...遍歷鏈表,到7的時候發(fā)現(xiàn)next 為null,遍歷結(jié)束
2修改元素7的next 指針指向3... ...

通過上面的例子我們可以明顯看到在多線程下操作這個鏈表,有可能會導(dǎo)致讀線程讀到的數(shù)據(jù)不完整,只有從鏈表頭部到元素7的位置的數(shù)據(jù)。由此可見,不加入任何保護(hù)措施的多線程保護(hù),勢必會導(dǎo)致數(shù)據(jù)的混亂。為了避免數(shù)據(jù)一致性問題,我們就需要將操作該隊列的代碼放入同步塊內(nèi)(鎖的對象也就是這個鏈表實例),來確保同一時刻只有一個線程可以訪問該鏈表。

如何加鎖?

這里簡單的說下,一般我們都是使用synchronized(如果沒有特殊需求建議直接使用這個關(guān)鍵字,jdk新版本它真的很快),記住synchronized 鎖的就是對象頭。

簡單的說下,主要有下面幾種用法:

  • synchronized 放在方法上,鎖的是當(dāng)前synchronized 方法的對象實例

  • synchronized在static 方法上,鎖的是synchronized 方法的class 類對象,注意這里class 其實也是一個對象。

  • synchronized(this)在代碼塊中,鎖的是代碼塊括號內(nèi)的對象,這里this指的就是調(diào)用這個方法的類實例對象

三、 多線程中易犯的錯誤

1、鎖范圍過大

共享資源訪問完成后, 后續(xù)的代碼沒有放在synchronized同步代碼塊之外。 會導(dǎo)致當(dāng)前線程長期無效的占用該鎖, 而其它爭用該鎖的線程只能等待, 最終導(dǎo)致性能受到極大影響。

 public void test()
 {
 		synchronized(lock){
 		... ... //正在訪問共享資源
 		... ... //做其它耗時操作,但這些耗時操作與共享資源無關(guān)
 	}
 }

面對上面這種寫法,會導(dǎo)致此線程長期占有此鎖,從而導(dǎo)致其他線程只能等待,下面來討論下解決方法:

1)單CPU場景下,將不需要同步的耗時操作拿到同步塊外面,有的情況可以提升性能,有的卻不行。

  • CPU密集型的代碼 ,不存在磁盤IO/網(wǎng)絡(luò)IO等低CPU消耗的代碼。 這種情況下, CPU 99%都在執(zhí)行代碼。 因此縮小同步塊也不會帶來任何性能上的提升, 同時縮小同步塊也不會帶來性能上的下降。

  • IO密集型的代碼,在執(zhí)行不消耗CPU的代碼時,其實CPU屬于空閑狀態(tài)的。如果此時讓CPU工作起來就可以帶來整體上性能的提升。所以在這種情況下,就可以將不需要同步的耗時操作移到同步塊外面了。

2)多CPU場景下,將耗時的CPU操作拿到同步塊外面,總是可以提升性能的

  • CPU密集型的代碼,不存在IO操作等不消耗CPU的代碼片段。因為當(dāng)前是多CPU,其他CPU也可能是空閑的。所以在縮小同步塊的時候,也會讓其他線程盡快的執(zhí)行這段代碼從而帶來性能上的提升。

  • IO密集型的代碼,因為當(dāng)前PCU都是空閑的狀態(tài),所以將耗時的操作放在同步塊外面,一定會帶來整體上的性能提升。

當(dāng)然,不管怎么樣,縮小鎖的同步范圍對于系統(tǒng)來說都是百利而無一害的,因此上面的代碼應(yīng)該改為:

 public void test()
 {
 		synchronized(lock){
 		... ... //正在訪問共享資源
 	}
 		... ... //做其它耗時操作,但這些耗時操作與共享資源無關(guān)
 }

綜上所述,一個重點,就是只將訪問共享資源的代碼放在同步塊內(nèi),保證快進(jìn)快出。

2、死鎖的問題

死鎖要知道的:

  • 死鎖,簡單地說就是兩個線程或多個線程在同時等待被對方持有的鎖導(dǎo)致的,死鎖會導(dǎo)致線程無法繼續(xù)執(zhí)行并被永久掛起。

  • 如果線程發(fā)生了死鎖,那我們就能從線程堆棧中明顯的看到”Found one Java-level deadlock“,并且線程棧還會給出死鎖的分析結(jié)果。

  • 死鎖這種問題如果發(fā)生在關(guān)鍵系統(tǒng)上就可能會導(dǎo)致系統(tǒng)癱瘓,如果想要快速恢復(fù)系統(tǒng),臨時唯一的方法就是保留線程棧先重啟,然后再盡快的恢復(fù)。

  • 死鎖這種問題有時候測試是很難被立即發(fā)現(xiàn)的,很多時候在測試時能否及時發(fā)現(xiàn)這類問題,就全看你的運氣和你準(zhǔn)備的測試用例了。

  • 避免死鎖這類問題,唯一的辦法就是改代碼。但一個可靠的系統(tǒng)是設(shè)計出來的,而不是通過改BUG改出來的,當(dāng)出現(xiàn)這種問題的時候就需要從系統(tǒng)設(shè)計角度去分析了。

  • 有人會認(rèn)為死鎖會導(dǎo)致CPU 100%,其實對也不對。 要看使用的什么類型的鎖了,比如synchronized導(dǎo)致的死鎖,那就不會導(dǎo)致CPU100%,只會掛起線程。但如果是自旋鎖這種才可能會消耗CPU。

3、共用一把鎖的問題

就是多個共享變量會共用一把鎖,特別是在方法級別上使用synchronized,從而人為導(dǎo)致的鎖競爭。

上例子,下面是新手容易犯的錯誤:

1 public class MyTest
2 {
3 Object shared;
4 synchronized void fun1() {...} //訪問共享變量shared
5 synchronized void fun2() {...} //訪問共享變量shared
6 synchronized void fun3() {...} //不訪問共享變量shared
7 synchronized void fun4() {...} //不訪問共享變量shared
8 synchronized void fun5() {...} //不訪問共享變量shared
9 }

上面的代碼每一個方法都被加了synchronized ,明顯違背了保護(hù)什么鎖什么的原則。

三、線程數(shù)我們一般設(shè)多少比較合理呢?

其實大家都知道,在大多數(shù)場合下多線程都是可以提高系統(tǒng)的性能和吞吐量,但一個系統(tǒng)到底多少個線程才是合理的?

總的來說,線程數(shù)量太多太少其實都不太好,多了會因為線程頻繁切換導(dǎo)致開銷增大,有時候反而降低了系統(tǒng)性能。少了又會導(dǎo)致CPU資源不能充分的利用起來,性能沒有達(dá)到瓶頸。

所以,系統(tǒng)到底使用多少線程合適,是要看系統(tǒng)的線程是否能充分的利用了CPU。其實實際情況,是很多時候不消耗CPU,如:磁盤IO、網(wǎng)絡(luò)IO等。

磁盤IO、網(wǎng)絡(luò)IO相比CPU的速度,那簡直是相當(dāng)?shù)穆?,在?zhí)行IO的這段時間里CPU其實是空閑的。如果這時其他線程能把這空閑的CPU利用上,就可以達(dá)到提示系統(tǒng)性能和吞吐的目的。

其實上面我們也提到過,也就是兩種計算特性:

CPU密集型: 因為每個CPU都是高計算負(fù)載的情況,如果設(shè)置過多的線程反而會產(chǎn)生不必要的上下文切換。所以,一般線程我們會設(shè)置 CPU 核數(shù) + 1就可以了,為啥要加1 呢,即使當(dāng)計算(CPU)密集型的線程偶爾由于頁缺失故障或者其他原因而暫停時,這個“額外”的線程也能確保 CPU 的時鐘周期不會被浪費,其實就是個備份。

IO密集型:因為大量的IO操作,會導(dǎo)致CPU處于空閑狀態(tài),所以這時我們可以多設(shè)置些線程。 所以, 線程數(shù) = CPU 核心數(shù) * (1+ IO 耗時/CPU 耗時) 就可以了,希望能給你點啟發(fā)。

“java多線程中易犯的錯誤是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI