Java并发编程的艺术(一)——并发编程需要注意的问题
并發(fā)是為了提升程序的執(zhí)行速度,但并不是多線程一定比單線程高效,而且并發(fā)編程容易出錯。若要實現(xiàn)正確且高效的并發(fā),就要在開發(fā)過程中時刻注意以下三個問題:
- 上下文切換
- 死鎖
- 資源限制
接下來會逐一分析這三個問題,并給出相應(yīng)的解決方案。
問題一:上下文切換會帶來額外的開銷
線程的運行機制
- 一個CPU每個時刻只能執(zhí)行一條線程;
- 操作系統(tǒng)給每條線程分配不同長度的時間片;
- 操作系統(tǒng)會從一堆線程中隨機選取一條來執(zhí)行;
- 每條線程用完自己的時間片后,即使任務(wù)還沒完成,操作系統(tǒng)也會剝奪它的執(zhí)行權(quán),讓另一條線程執(zhí)行
什么是“上下文切換”?
當(dāng)一條線程的時間片用完后,操作系統(tǒng)會暫停該線程,并保存該線程相應(yīng)的信息,然后再隨機選擇一條新線程去執(zhí)行,這個過程就稱為“線程的上下文切換”。
上下文切換的過程
- 暫停正在執(zhí)行的線程;
- 保存該線程的相關(guān)信息(如:執(zhí)行到哪一行、程序計算的中間結(jié)果等)
- 從就緒隊列中隨機選一條線程;
- 讀取該線程的上下文信息,繼續(xù)執(zhí)行
上下文切換是有開銷的
每次進行上下文切換時都需要保存當(dāng)前線程的執(zhí)行狀態(tài),并加載新線程先前的狀態(tài)。?
如果上下文切換頻繁,CPU花在上下文切換上的時間占比就會上升,而真正處理任務(wù)的時間占比就會下降。?
因此,為了提高并發(fā)程序的執(zhí)行效率,讓CPU把時間花在刀刃上,我們需要減少上下文切換的次數(shù)。
如何減少上下文切換?
減少線程的數(shù)量?
由于一個CPU每個時刻只能執(zhí)行一條線程,而傲嬌的我們又想讓程序并發(fā)執(zhí)行,操作系統(tǒng)只好不斷地進行上下文切換來使我們從感官上覺得程序是并發(fā)執(zhí)的行。因此,我們只要減少線程的數(shù)量,就能減少上下文切換的次數(shù)。?
然而如果線程數(shù)量已經(jīng)少于CPU核數(shù),每個CPU執(zhí)行一條線程,照理來說CPU不需要進行上下文切換了,但事實并非如此。控制同一把鎖上的線程數(shù)量?
如果多條線程共用同一把鎖,那么當(dāng)一條線程獲得鎖后,其他線程就會被阻塞;當(dāng)該線程釋放鎖后,操作系統(tǒng)會從被阻塞的線程中選一條執(zhí)行,從而又會出現(xiàn)上下文切換。?
因此,減少同一把鎖上的線程數(shù)量也能減少上下文切換的次數(shù)。-
采用無鎖并發(fā)編程?
我們知道,如果減少同一把鎖上線程的數(shù)量就能減少上下文切換的次數(shù),那么如果不用鎖,是否就能避免因競爭鎖而產(chǎn)生的上下文切換呢??
答案是肯定的!但你需要根據(jù)以下兩種情況挑選不同的策略: - 需要并發(fā)執(zhí)行的任務(wù)是無狀態(tài)的:HASH分段?
所謂無狀態(tài)是指并發(fā)執(zhí)行的任務(wù)沒有共享變量,他們都獨立執(zhí)行。對于這種類型的任務(wù)可以按照ID進行HASH分段,每段用一條線程去執(zhí)行。 - 需要并發(fā)執(zhí)行的任務(wù)是有狀態(tài)的:CAS算法?
如果任務(wù)需要修改共享變量,那么必須要控制線程的執(zhí)行順序,否則會出現(xiàn)安全性問題。你可以給任務(wù)加鎖,保證任務(wù)的原子性與可見性,但這會引起阻塞,從而發(fā)生上下文切換;為了避免上下文切換,你可以使用CAS算法, 僅在線程內(nèi)部需要更新共享變量時使用CAS算法來更新,這種方式不會阻塞線程,并保證更新過程的安全性。
問題二:并發(fā)不當(dāng)可能會產(chǎn)生死鎖
什么是“死鎖”?
當(dāng)多個線程相互等待已經(jīng)被對方占用的資源時,就會產(chǎn)生死鎖。
死鎖示例
class DeadLock {// 鎖A private Object lockA;// 鎖Bprivate Object lockB;// 第一條線程Thread t1 = new Thread(new Runnable(){void run () {synchronized (lockA) {Thread.sleep(5000);synchronized (lockB) {System.out.println("線程1");}}}}).start();// 第二條線程Thread t2 = new Thread(new Runnable(){void run () {synchronized (lockB) {Thread.sleep(5000);synchronized (lockA) {System.out.println("線程2");}}}}).start(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 線程1和線程2都需要鎖A和鎖B
- 線程1首先獲得鎖A,然后sleep 5秒?
PS:線程sleep過程中會釋放執(zhí)行權(quán) - 此時線程2執(zhí)行,獲得鎖B,然后也sleep 5秒;
- 線程1 sleep 5秒后繼續(xù)執(zhí)行,此時需要鎖B,然而鎖B已經(jīng)被線程2持有,因此線程1被阻塞;
- 此時線程2醒了,它需要鎖A,然而鎖A已經(jīng)被線程1持有,因此它也被阻塞;
- 此時死鎖出現(xiàn)了!兩條線程相互等待已經(jīng)被占用的資源,程序就死在這了。?
死鎖是并發(fā)編程中一個重要的問題,上面介紹的減少上下文切換只是為了提升程序的性能,而一旦產(chǎn)生死鎖,程序就不能正確執(zhí)行!
如何避免死鎖?
- 不要在一條線程中嵌套使用多個鎖;
- 不要在一條線程中嵌套占用多個計算機資源;
- 給鎖和資源加超時時間?
如果你非要在一條線程中嵌套使用多個鎖或占用多個資源,那你需要給鎖、資源加超時時間,從而避免無限期的等待。
問題三:計算機資源會限制并發(fā)
誤區(qū):線程越多速度越快
在并發(fā)編程中,并不是線程越多越好,有時候線程多了反而會拉低執(zhí)行效率,原因如下:
- 線程多了會導(dǎo)致上下文切換增多,CPU花在上下文切換的時間增多后,花在處理任務(wù)上的時間自然就減少了。
- 計算機資源會限制程序的并發(fā)度。?
- 比如:你家網(wǎng)入口帶寬10M,你寫了個多線程下載的軟件,同時開100條線程下載,那每條線程平均以每秒100k的速度下載,然而100條線程之間還要不斷進行上下文切換,所以你還不如只開5條線程,每條平均2M/s的速度下載。
- 再比如:數(shù)據(jù)庫連接池最多給你用10個連接,然而你卻開了100條線程進行數(shù)據(jù)庫操作,那么當(dāng)10個用完后其他線程就要等待,從而操作系統(tǒng)要在這100條線程間不斷進行上下文切換;所以與其這樣還不如只開10條線程,減少上下文切換的次數(shù)。
說了這么多只想告訴你一個道理:線程并不是越多越好,要根據(jù)當(dāng)前計算機所能提供的資源考慮。
什么是“資源”?
資源分為硬件資源和軟件資源:
- 硬件資源?
- 硬盤讀寫速度
- 網(wǎng)絡(luò)帶寬
- 等
- 軟件資源?
- Socket連接數(shù)
- 數(shù)據(jù)庫連接數(shù)
- 等
如何解決資源的限制?
- 花錢買更高級的機器
- 根據(jù)資源限制并發(fā)度
總結(jié)
以上是生活随笔為你收集整理的Java并发编程的艺术(一)——并发编程需要注意的问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql内存不断被占用,导致每隔一个多
- 下一篇: 在CentOS 7系统里使用465端口发