一開始我們做的都是「順序編程」,但是有時候程式純順序執行的性能並不高,並且對於部分問題順序執行程式並不能很好地解決。 這時候「併發」就是一個很好的解決方案了,「併發」的含義其實很簡單,即並行地執行程式中的多個部分。這些部分要麼看起來在併發地執行(單處理器環境下通過競爭 cpu 時間片實現同時執行效果 ...
一開始我們做的都是「順序編程」,但是有時候程式純順序執行的性能並不高,並且對於部分問題順序執行程式並不能很好地解決。
這時候「併發」就是一個很好的解決方案了,「併發」的含義其實很簡單,即並行地執行程式中的多個部分。這些部分要麼看起來在併發地執行(單處理器環境下通過競爭 cpu 時間片實現同時執行效果),要麼在多處理器環境下真正同時執行。
併發「具有可論證的確定性,但是實際上具有不可確定性」。這是研究併發問題的最強理由:如果視而不見,你就會遭其反噬。-- 《Java 編程思想》
雖然書里是這麼講,但是在實際開發當中,碰到「不可確定性」的概率比較低(可能我經驗不夠?),通常碰到的問題都是由於考慮問題不全面,代碼 bug 導致,很少碰到所謂的「不可確定性」。當然,雖然沒碰到過,不過據說碰到了可能是非常坑爹的,通常連復現問題都比較困難。
有時候,雖然你沒有主動開啟線程使用併發,但是你還是無法避免併發,Java Web 開發中基本的 Web 類庫、Servlet 具有天生的多線程性。
併發的多面性
更快的執行
併發通常是用來提高運行在「單處理器」上的程式的性能,但是通常在單處理器上運行的併發程式比該程式的所有部分都順序執行的開銷大,因為增加了所謂的「上下文切換」的代價。那既然開銷變大,又怎麼會提高性能呢?答案是:「阻塞」,最典型的就是 I/O。從性能的角度看,如果沒有任務會阻塞,那麼在單處理器機器上使用併發就沒有任何意義。
在單處理器系統中的性能提高的常見示例是「事件驅動的編程」。如果不使用併發,則產生可響應用戶界面的唯一方式就是所有的任務都周期性地檢查用戶的輸入。通過創建單獨的執行線程來響應用戶的輸入,即使這個線程在大多數時間里都是阻塞的,但是程式可以保證具有一定程度的可響應性。—— 《Java 編程思想》
更多的困難
實現併發最直接的方式是在操作系統級別使用進程。操作系統通常會將進程互相隔離開,進程使用的資源也都是隔離的,進程間相互影響較小,編程也相對簡單。與此相反的是,像 Java 所使用的這種併發系統會共用諸如記憶體和 I/O 這樣的資源,因此編寫多線程程式最基本的困難在於在協調不同線程驅動的任務之間對這些資源的使用,以使得這些資源不會同時被多個任務訪問。
改進代碼設計
多線程系統對可用的線程數量的限制通常都會是一個相對較小的數字,有時就是數十或者數百這樣的數量級。這個數字在程式控制範圍之外可能會發生變化——它可能依賴於平臺,或者在 Java 中,依賴於 Java 的版本。
在 Java 中,通常要假定你不會獲得足夠多的線程,從而使得可以為程式中的每個任務都提供一個線程。解決這個問題的典型方式是使用「協作多線程」。
Java 的線程機制是「搶占式」的,這表示調度機制會周期性地中斷線程,將上下文切換到另一個線程,從而為每個線程都提供時間片。
在協作式系統中,每個任務都會自動地放棄控制,這要求程式員要有意識地在每個任務中插入某種類型的讓步語句。
協作式系統的優勢是雙重的:上下文切換的開銷通常比搶占式系統要低廉許多,並且對可以同時執行的線程數量在理論上沒有任何限制。
併發需要付出代價,包含複雜性代價,但是這些代價與在程式設計、資源負載均衡以及用戶方便使用方面的改進相比,就顯得微不足道了。線程使你能夠創建更加鬆散耦合的設計,否則,你的代碼中各個部分都必須顯式地關註那些通常可以由線程來處理的任務。