1. 程式人生 > >併發程式設計——如何讓程式最大程度的併發執行?

併發程式設計——如何讓程式最大程度的併發執行?

本帥博主和夥伴們正在學習,遇上一個較為冗長的程式例子,又必須打出來,但是又不想佔用太多時間。像我這樣,既想要敲出這個程式,又不想佔用自己太多時間,該怎麼辦呢?這就需要併發程式設計來幫忙了。A君寫一段,B君寫一段,C君再寫一段,湊起來,就變成一個完成的程式啦,這樣就提升了效率。

看完上面這個例子,大家也許就對併發程式設計有了一個大概的瞭解, 但併發程式設計到底是什麼呢?

這段時間本帥博主一直在JVM和併發程式設計中來回切換學習總結,前幾篇部落格總結了一下關於JVM記憶體分配和垃圾回收的知識,今天就來了解一下併發程式設計裡的基礎知識。

一個東西存在就有它存在的理由,那麼為什麼要用併發程式設計呢

這一點從上面這個例子就可以看出來,我們這樣一起敲程式碼,使得我們這個“敲出這個完整程式”的任務完成時間大幅減少。

併發程式設計的目的是為了讓程式執行的更快,但是啟動更多的執行緒不一定會讓程式能夠最大程度的併發執行,甚至有時候,併發比序列還要慢。而在使用併發程式設計的時候,我們也會面臨許多挑戰。是什麼挑戰呢?且看下文。

1.上下文切換

多執行緒聽起來似乎是需要多個處理器,但其實並不是,單核處理器也是可以支援多執行緒執行程式碼的。因為CPU通過分配時間片來實現多執行緒的。

時間片是 CPU 分配給各個執行緒的時間, CPU 會通過不斷切換執行緒執行。而因為時間片很短,讓我們感覺多個執行緒是同時執行的。

每次時間片切換之前都會儲存上一個任務的狀態(這裡是通過程式計數器來記錄執行緒進行到哪一步的,如果大家對程式計數器不瞭解,可以參考:JVM——記憶體模型(一):程式計數器),這樣下一次切換回這個任務的時候,可以再次載入這個任務的狀態。而這個儲存到載入的過程就是一次上下文切換

這樣的切換是會影響多執行緒的執行效率的。想象我們看一本英文書籍,如果遇到單詞不認識,我們會去查閱詞典,但是在查閱之前,我們得先記住我們看到那一頁了,以便等到查到單詞後還能繼續在之前看的位置讀下去。雖然這樣能夠保證閱讀的連貫性,但閱讀的速度必然是受到影響的。

多執行緒併發執行不一定比序列執行快。測試發現,序列和並行做同一件迴圈操作,在達到一定的迴圈次數之前,併發是沒有序列速度快的。這正是因為執行緒的建立以及上下文切換有開銷的緣故。

那麼怎麼度量上下文帶來的消耗呢?我們可以:

  • 使用 Lmbench3 可以測量上下文切換的時長
  • 使用 vmstat 可以測量上下文切換的次數

那麼如何減少上下文的切換呢?

  • 無鎖併發程式設計。多執行緒競爭鎖時,會引起上下文切換,所以可以用一些方法來避免使用鎖。例如將資料的 ID 按 Hash 演算法取模分段,不同的執行緒處理不同段的資料。
  • CAS 演算法。Java 的 Atomic 包使用 CAS 演算法來更新資料,不需要加鎖
  • 使用最少執行緒:避免建立不必要的執行緒,如果建立了很多多餘的執行緒,將會造成大量的執行緒處於等待狀態。
  • 協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間切換。

2.避免死鎖

鎖是個非常有用的工具,使用也很簡單易懂,不過,要是使用不當,可能會引起死鎖,從而導致系統不可用。

在一些複雜的場景中,可能會遇到死鎖問題,比如執行緒 T1拿到鎖之後,因為一些異常情況沒有釋放鎖(例如死迴圈)。又或者是 T2拿到了一個數據庫鎖,但釋放鎖的時候丟擲了異常,沒有釋放掉,這些情況都會出現死鎖。

一旦出現死鎖,業務是可以感知的,因為無法繼續提供服務了,我們可以通過 dump 現場來檢視哪個執行緒除了問題,並根據日誌資訊進行跟蹤程式碼。

以下是幾個常見的避免死鎖的方法:

  • 避免一個執行緒同時獲取多個鎖
  • 避免一個執行緒在鎖內佔用多個資源,儘量保證每個鎖只佔用一個資源
  • 嘗試使用定時鎖,即使用 lock.tryLock(timeout) 來替代使用內部鎖機制。
  • 資料庫鎖的加鎖解鎖必須在一個數據庫連線裡,否則會出現解鎖失敗(即上文說到的)的情況。

3.資源限制的挑戰

資源限制可以分為計算機硬體資源軟體資源

  • 硬體資源限制有寬頻速度硬碟讀寫速度CPU 處理速度。所以在下載東西的時候,雖然寬頻速度只有 2Mb/s,某個資源下載速度是 1Mb/s,但系統即使啟動了 10 個執行緒下載資源,下載速度也不會變成10Mb/s,。
  • 軟體資源限制有資料庫的連線數socket 連線等。

因為資源限制的原因,有時候會導致併發執行的任務因為資源不足,甚至還沒有序列執行速度快。比如因為開了多執行緒導致了一些資源的請求,但資源又不夠用,就會導致一些資源排程和上下文切換的開銷,從而降低了執行速度。

我們知道,限制從來都可以最小化,那怎麼將這些限制最小化呢? 

  • 對於硬體資源限制,可以使用叢集並行執行程式。
  • 對於軟體資源,可以考慮使用資源池將資源複用。在資源限制的情況下,要根據不同的資源限制調整程式的併發度。例如有資料庫操作時,設計資料庫連線數,如果 SQL 語句執行的非常快,但執行緒的數量要比資料庫連線數大很多,那麼某些執行緒將會被阻塞,等待資料庫連線。

4.總結

併發程式設計有很多挑戰,如果併發程式寫的不嚴謹,出現了問題,定位和解決起來都比較棘手和耗時。所以對於我等 Java 開發工程師來說,要多多使用 JDK 併發包提供的併發容器和工具類來解決併發問題,因為這些類已經通過了充分的測試和優化,解決上文中描述的問題,那是幾乎是沒有問題的。