1. 程式人生 > >一、併發程式設計的挑戰

一、併發程式設計的挑戰

由於最近在看《Java併發程式設計的藝術》,為了強迫自己看下去,就看著書,寫著部落格,也能把看書時自己的一些觀點加入其中,方便理解和記憶。

Java併發程式設計的目的是為了讓程式執行的更快,但不是啟動更多的執行緒就能讓程式最大限度的執行,因為在併發程式設計中,啟動更多的執行緒,會面臨上下文切換、死鎖、以及所使用的軟體和硬體的資源限制等問題。
1.1上下文切換
單核處理器下,多執行緒執行程式碼,cpu給每個執行緒分配cpu時間片來實現多執行緒,cpu不停地切換執行緒執行,讓我們感覺多個執行緒是同時執行的。
Cpu通過時間片分配演算法來迴圈執行任務,當任務A執行一個時間片後會切換到下一個任務B,切換前會儲存任務A的狀態, 方便下次切換回這個任務時,可以再載入這個任務的狀態。任務A從儲存到再載入的過程就是一次上下文切換。
生活中的例子:讀書時,發現生詞,查字典,但是必須記住自己看到哪裡了,方便查完字典繼續看書,這種切換影響讀書效率,同樣上下文切換影響多執行緒的執行速度。

多執行緒一定快嗎?
使用多執行緒進行併發執行,不一定會比順序執行快,因為執行緒有建立和上下文切換的時間開銷。

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

1.2 死鎖
鎖是個非常有用的工具,運用場景非常多,因為它使用起來非常簡單,而且易於理解。但同時它也會帶來一些困擾,那就是可能會引起死鎖,一旦產生死鎖,就會造成系統功能不可用。讓我們先來看一段程式碼,這段程式碼會引起死鎖,使執行緒t1和執行緒t2互相等待對方釋放鎖。

public class DeadLockDemo {
    private static String A = "A";
    private static String B = "B";
    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }
    private
void deadLock() { Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (A) { try { System.out.println(Thread.currentThread().getName()); Thread.currentThread().sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { System.out.println("1"); } } } },"aaa"); Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (B) { synchronized (A) { System.out.println("2"); } } } }); t1.start(); t2.start(); } }

這裡寫圖片描述
這段程式碼只是演示死鎖的場景,在現實中你可能不會寫出這樣的程式碼。但是,在一些更為複雜的場景中,你可能會遇到這樣的問題,比如t1拿到鎖之後,因為一些異常情況沒有釋放鎖(死迴圈)。又或者是t1拿到一個數據庫鎖,釋放鎖的時候丟擲了異常,沒釋放掉。

一旦出現死鎖,業務是可感知的,因為不能繼續提供服務了,那麼只能通過dump執行緒檢視到底是哪個執行緒出現了問題,以下執行緒資訊告訴我們是DeadLockDemo類的第42行和第31行引起的死鎖。

"Thread-2" prio=5 tid=7fc0458d1000 nid=0x116c1c000 waiting for monitor entry [116c1b000
java.lang.Thread.State: BLOCKED (on object monitor)
at com.ifeve.book.forkjoin.DeadLockDemo$2.run(DeadLockDemo.java:42)
- waiting to lock <7fb2f3ec0> (a java.lang.String)
- locked <7fb2f3ef8> (a java.lang.String)
at java.lang.Thread.run(Thread.java:695)
"Thread-1" prio=5 tid=7fc0430f6800 nid=0x116b19000 waiting for monitor entry [116b18000
java.lang.Thread.State: BLOCKED (on object monitor)
at com.ifeve.book.forkjoin.DeadLockDemo$1.run(DeadLockDemo.java:31)
- waiting to lock <7fb2f3ef8> (a java.lang.String)
- locked <7fb2f3ec0> (a java.lang.String)
at java.lang.Thread.run(Thread.java:695)

避免死鎖的常見方法
1、避免一個執行緒同時獲取多個鎖
2、避免一個執行緒在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源
3、嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制
4、對於資料庫鎖,加鎖和解鎖必須在一個數據庫連線裡,否則會出現解鎖失敗的情況

1.3 資源限制的挑戰
1)什麼是資源限制
在併發程式設計中,程式的執行速度受限於硬體或軟體資源
例如:伺服器頻寬只有2Mb/s,某個資源的下載速度是1Mb/s,為了加快下載速度,啟動了10個執行緒下載資源,下載速度不會變成10Mb/s。
硬體資源限制有:頻寬的上傳/下載速度、硬碟讀寫速度、cpu的處理速度
軟體資源限制有:資料庫的連線數、socket連線數等。
2)資源限制引發的問題
在併發程式設計中,加快程式碼執行速度的原則是將程式碼中序列執行的部分變成併發執行,但是如果將某段序列的程式碼併發執行,由於受限於資源,仍然在序列執行,這時候程式反而會更慢,因為增加了上下文切換和資源排程的時間。
3)如何解決資源限制的問題
對於硬體資源限制,可以考慮使用叢集並行執行程式。既然單機的資源有限制,那麼就讓程式在多機上執行。比如使用ODPS、Hadoop或者自己搭建伺服器叢集,不同的機器處理不同的資料,可以通過“資料ID/機器數”,計算得到一個機器編號,然後由對應編號的機器處理這筆資料。
4)在資源限制情況下進行併發程式設計
根據不同的資源限制調整程式的併發度,比如下載檔案程式依賴於兩個資源–頻寬和硬碟讀寫速度。有資料庫操作時,涉及資料庫連線數,如果sql執行非常快,而執行緒的數量比資料庫連線數大很多,則某些執行緒會被阻塞,等待資料庫連線。

參考 《Java併發程式設計的藝術》