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