并發編程的三大核心問題
并發編程并不是一項孤立存在的技術,也不是脫離現實生活場景而提出的一項技術。
相反,并發編程是一項綜合性的技術,同時,它與現實生活中 的場景有著緊密的聯系。
并發編程有三大核心問題:
- 分工問題
- 同步問題
- 互斥問題
本文就對這三大核心問題進行簡單的介紹。
1 分工問題
關于分工,比較官方的解釋是:一個比較大的任務被拆分成多個大小合適的任務,這些大小合適的任務被交給合適的線程去執行。
分工強調的是執行的性能。
▊ 類比現實案例
可以類比現實生活中的場景來理解分工,例如,如果你是一家上市公司的 CEO,那么,你的主要工作就是規劃公司的戰略方向和管理好公司。就如何管理好公司而言,涉及的任務就比較多了。
這里,可以將管理好公司看作一個很大的任務,這個很大的任務可以包括人員招聘與管理、 產品設計、產品開發、產品運營、產品推廣、稅務統計和計算等。如果將這些工作任務都交給 CEO一個人去做,那么估計 CEO 會被累趴下的。CEO一人做完公司所有日常工作如圖1所示。
圖1 CEO 一人做完公司所有日常工作
如圖1 所示,公司 CEO 一個人做完公司所有日常工作是一種非常不可取的方式,這將導致公司無法正常經營,那么應該如何做呢?
有一種很好的方式是分解公司的日常工作,將人員招聘與管理工作交給人力資源部,將產 品設計工作交給設計部,將產品開發工作交給研發部,將產品運營和產品推廣工作分別交給運 營部和市場部,將公司的稅務統計和計算工作交給財務部。
這樣,CEO 的重點工作就變成了及時了解各部門的工作情況,統籌并協調各部門的工作, 并思考如何規劃公司的戰略。
公司分工后的日常工作如圖2所示。
圖2 公司分工后的日常工作
將公司的日常工作分工后,可以發現,各部門之間的工作是可以并行推進的。例如,在人力資源部進行員工的績效考核時,設計部和研發部正在設計和開發公司的產品,與此同時,公司的運營人員正在和設計人員與研發人員溝通如何更好地完善公司的產品,而市場部正在加大力度宣傳和推廣公司的產品,財務部正在統計和計算公司的各種財務報表等。一切都是那么有條不紊。
所以,在現實生活中,安排合適的人去做合適的事情是非常重要的。映射到并發編程領域 也是同樣的道理。
▊ 并發編程中的分工
在并發編程中,同樣需要將一個大的任務拆分成若干比較小的任務,并將這些小任務交給 不同的線程去執行,如圖3所示。
圖3 將一個大的任務拆分成若干比較小的任務
在并發編程中,由于多個線程可以并發執行,所以在一定程度上能夠提高任務的執行效率。
在并發編程領域,還需要注意一個問題就是:將任務分給合適的線程去做。也就是說,該由主線程執行的任務不要交給子線程去做,否則,是解決不了問題的。
這就好比一家公司的 CEO 將規劃公司未來的工作交給一位產品開發人員一樣,不僅不能規劃好公司的未來,甚至會與公司的價值觀背道而馳。
在 Java 中,線程池、Fork/Join 框架和 Future 接口都是實現分工的方式。在多線程設計模式中,Guarded Suspension 模式、Thread-Per-Message 模式、生產者—消費者模式、兩階段終止模式、Worker-Thread 模式和 Balking 模式都是分工問題的實現方式。
2 同步問題
在并發編程中,同步指一個線程執行完自己的任務后,以何種方式來通知其他的線程繼續執行任務,也可以將其理解為線程之間的協作,同步強調的是執行的性能。
▊ 類比現實案例
可以在現實生活中找到與并發編程中的同步問題相似的案例。
例如,張三、李四和王五共同開發一個項目,張三是一名前端開發人員,他需要等待李四的開發接口任務完成再開始渲染 頁面,而李四又需要等待王五的服務開發工作完成再寫接口。
也就是說,任務之間是存在依賴關系的,前面的任務完成后,才能執行后面的任務。
在現實生活中,這種任務的同步,更多的是靠人與人之間的交流和溝通來實現的。例如,王五的服務開發任務完成了,告訴李四,李四馬上開始執行開發接口任務。等李四的接口開發完成后,再告訴張三,張三馬上調用李四開發的接口將返回的數據渲染到頁面上。現實生活中 的同步模型如圖4所示。
圖4 現實生活中的同步模型
由圖4可以看出,在現實生活中,張三、李四和王五的任務之間是有依賴關系的,張三渲染頁面的任務依賴李四開發接口的任務完成,李四開發接口的任務依賴王五開發服務的任務完成。
▊ 并發編程中的同步
在并發編程領域,同步機制指一個線程的任務執行完成后,通知其他線程繼續執行任務的方式,并發編程同步簡易模型如圖5所示。
圖5 并發編程同步簡易模型
由圖5可以看出,在并發編程中,多個線程之間的任務是有依賴關系的。
線程 A 需要阻塞等待線程 B 執行完任務才能開始執行任務,線程 B 需要阻塞等待線程 C 執行完任務才能開始執行任務。線程 C 執行完任務會喚醒線程 B 繼續執行任務,線程 B 執行完任務會喚醒線程 A 繼續執行任務。
這種線程之間的同步機制,可以使用如下的 if 偽代碼來表示。
if(依賴的任務完成){
執行當前任務
}else{
繼續等待依賴任務的執行
}
上述 if 偽代碼所代表的含義是:當依賴的任務完成時,執行當前任務,否則,繼續等待依 賴任務的執行。
在實際場景中,往往需要及時判斷出依賴的任務是否已經完成,這時就可以使用 while 循 環來代替 if 判斷, while 偽代碼如下。
while(依賴的任務未完成){
繼續等待依賴任務的執行
}
執行當前任務
上述 while 偽代碼所代表的含義是:如果依賴的任務未完成,則一直等待,直到依賴的任務完成,才執行當前任務。
在并發編程領域,同步機制有一個非常經典的模型——生產者—消費者模型。如果隊列已滿,則生產者線程需要等待,如果隊列不滿,則需要喚醒生產者線程;如果隊列為空,則消費者線程需要等待,如果隊列不為空,則需要喚醒消費者。
可以使用下面的偽代碼來表示生產者—消費者模型。
- 生產者偽代碼
while(隊列已滿){
生產者線程等待
}
喚醒生產者
- ?消費者偽代碼
while(隊列為空){
消費者等待
}
喚醒消費者
在Java 中,Semaphore、Lock、synchronized.、CountDownLatch、CyclicBarrier、Exchanger 和 Phaser 等工具類或框架實現了同步機制。
3 互斥問題
在并發編程中,互斥問題一般指在同一時刻只允許一個線程訪問臨界區的共享資源。互斥強調的是多個線程執行任務時的正確性。
▊ 類比現實案例
互斥問題在現實中的一個典型場景就是交叉路口的多輛車匯入一個單行道,如圖6所示。
圖6 交叉路口的多輛車匯入一個單行道
從圖6 可以看出,當多輛車經過交叉路口匯入同一個單行道時,由于單行道的入口只能容納一輛車通過,所以其他的車輛需要等待前面的車輛通過單行道入口后,再依次有序通過單行道入口。這就是現實生活中的互斥場景。
▊ 并發編程中的互斥
在并發編程中,分工和同步強調的是任務的執行性能,而互斥強調的則是執行任務的正確性,也就是線程的安全問題。
如果在并發編程中,多個線程同時進入臨界區訪問同一個共享變量,則可能產生線程安全問題,這是由線程的原子性、可見性和有序性問題導致的。
而在并發編程中解決原子性、可見性和有序性問題的核心方案就是線程之間的互斥。
例如,可以使用JVM中提供的synchronized鎖來實現多個線程之間的互斥,使用synchronized鎖的偽代碼如下。
- 修飾方法
public synchronized void methodName(){
//省略具體方法
}
- 修飾代碼塊
public void methodName(){
synchronized(this){
//省略具體方法
}
}
public void methodName(){
synchronized(obj){
//省略具體方法
}
}
public void methodName(){
synchronized(ClassName.class){
//省略具體方法
}
}
- 修飾靜態方法
public synchronized static void staticMethodName(){
//省略具體方法
}
除了 synchronized 鎖,Java 還提供了 ThreadLocal、CAS、原子類和以CopyOnWrite 開頭的并發容器類、Lock 鎖及讀/寫鎖等,它們都實現了線程的互斥機制。