併發程式設計的目的是為了讓程式執行得更快,但並不是啟動更多的執行緒就能讓程式最大限度地併發執行。在進行併發程式設計時,如果希望通過過執行緒執行任務讓程式執行得更快,會面臨非常多的挑戰,比如上下文切換的問題、死鎖的問題,以及受限於硬體和軟體的資源限制問題。
上下文切換
即使是單核處理器也支援多執行緒執行程式碼,CPU通過給每個執行緒分配CPU時間片來實現這個機制。時間片是CPU分配給各個執行緒的時間,因為時間片非常短,所以CPU通過不停地切換執行緒執行,讓我們感覺多個執行緒是同時執行的,時間片一般是幾十毫秒(ms)。
CPU通過時間片分配演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會儲存上一個任務的狀態,以便下次切換回這個任務時,可以再載入這個任務的狀態。所以任務從儲存到再載入的過程就是一次上下文切換。
上下文切換會影響多執行緒的執行速度,減少上下文切換的方法有無鎖併發程式設計、CAS演算法、使用最少執行緒和使用協程。
-
無鎖併發程式設計
多執行緒競爭鎖時,會引起上下文切換,所以多執行緒處理資料時,可以用一些辦法來避免使用鎖,如將資料的ID按照Hash演算法取模分段,不同的執行緒處理不同段的資料。
-
CAS演算法
Java的Atomic包使用CAS演算法來更新資料,而不需要加鎖。
-
使用最少執行緒
避免建立不需要的執行緒,比如任務很少,但是建立了很多執行緒來處理,這樣會造成大量執行緒都處於等待狀態。
-
協程
在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換。
死鎖
鎖是個非常有用的工具,運用場景非常多,因為它使用起來非常簡單,而且易於理解。但同時它也會帶來一些困擾,那就是可能會引起死鎖,一旦產生死鎖,就會造成系統功能不可用。
死鎖程式碼示例:
public class DeadLock{
private Object lockOne = new Object();
private Object lockTwo = new Object();
public void methodOne() {
synchronized (lockOne){
try {
Thread.sleep(5000l);
} catch (InterruptedException e) {
//
}
methodTwo();
}
}
public void methodTwo() {
synchronized (lockTwo){
try {
Thread.sleep(5000l);
} catch (InterruptedException e) {
}
methodOne();
}
}
public static void main(String[] args) {
DeadLock deadLock = new DeadLock();
new Thread(new Runnable() {
@Override
public void run() {
deadLock.methodOne();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
deadLock.methodTwo();
}
}).start();
}
}
複製程式碼
一旦出現死鎖,業務是可感知的,因為不能繼續提供服務了,那麼只能通過dump執行緒檢視到底是哪個執行緒出現了問題。避免死鎖的幾個常見方法:
-
避免一個執行緒同時獲取多個鎖
-
避免一個執行緒在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
-
嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
-
對於資料庫鎖,加鎖和解鎖必須在一個資料庫連線裡,否則會出現解鎖失敗的情況。
資源限制的挑戰
-
什麼是資源限制
資源限制是指在進行併發程式設計時,程式的執行速度受限於計算機硬體資源或軟體資源。例如,伺服器的頻寬只有2Mb/s,某個資源的下載速度是1Mb/s每秒,系統啟動10個執行緒下載資源,下載速度不會變成10Mb/s,所以在進行併發程式設計時,要考慮這些資源的限制。硬體資源限制有頻寬的上傳/下載速度、硬碟讀寫速度和CPU的處理速度。軟體資源限制有資料庫的連線數和socket連線數等。
-
資源限制引發的問題
在併發程式設計中,將程式碼執行速度加快的原則是將程式碼中序列執行的部分變成併發執行,但是如果將某段序列的程式碼併發執行,因為受限於資源,仍然在序列執行,這時候程式不僅不會加快執行,反而會更慢,因為增加了上下文切換和資源排程的時間。
-
如何解決資源限制的問題
對於硬體資源限制,可以考慮使用叢集並行執行程式。既然單機的資源有限制,那麼就讓程式在多機上執行。比如使用ODPS、Hadoop或者自己搭建伺服器叢集,不同的機器處理不同的資料。可以通過“資料ID%機器數”,計算得到一個機器編號,然後由對應編號的機器處理這筆資料。對於軟體資源限制,可以考慮使用資源池將資源複用。比如使用連線池將資料庫和Socket連線複用,或者在呼叫對方webservice介面獲取資料時,只建立一個連線。
-
在資源限制情況下進行併發程式設計
如何在資源限制的情況下,讓程式執行得更快呢?方法就是,根據不同的資源限制調整程式的併發度,比如下載檔案程式依賴於兩個資源——頻寬和硬碟讀寫速度。有資料庫操作時,涉及資料庫連線數,如果SQL語句執行非常快,而執行緒的數量比資料庫連線數大很多,則某些執行緒會被阻塞,等待資料庫連線。
參考資料
Java併發程式設計的藝術
歡迎留言補充,共同交流。個人微信公眾號求關注: