Java多線程特性為構建高性能的應用提供瞭極大的方便,但是也帶來瞭不少的麻煩。線程間同步、數據一致性等煩瑣的問題需要細心的考慮,一不小心就會出現一些微妙的,難以調試的錯誤。
另外,應用邏輯和線程邏輯糾纏在一起,會導致程序的邏輯結構混亂,難以復用和維護。本文試圖給出一個解決這個問題的方案,通過構建一個並發模型框架(framework),使得開發多線程的應用變得容易。
基礎知識
Java語言提供瞭對於線程很好的支持,實現方法小巧、優雅。對於方法重入的保護,信號量(semaphore)和臨界區(critical section)機制的實現都非常簡潔。可以很容易的實現多線程間的同步操作從而保護關鍵數據的一致性。這些特點使得Java成為面向對象語言中對於多線程特性支持方面的佼佼者(C++正在試圖把boost庫中的對於線程的支持部分納入語言標準)。
Java中內置瞭對於對象並發訪問的支持,每一個對象都有一個監視器(monitor),同時隻允許一個線程持有監視器從而進行對對象的訪問,那些沒有獲得監視器的線程必須等待直到持有監視器的線程釋放監視器。對象通過synchronized關鍵字來聲明線程必須獲得監視器才能進行對自己的訪問。
synchronized聲明僅僅對於一些較為簡單的線程間同步問題比較有效,對於哪些復雜的同步問題,比如帶有條件的同步問題,Java提供瞭另外的解決方法,wait/notify/notifyAll。
獲得對象監視器的線程可以通過調用該對象的wait方法主動釋放監視器,等待在該對象的線程等待隊列上,此時其他線程可以得到監視器從而訪問該對象,之後可以通過調用notify/notifyAll方法來喚醒先前因調用wait方法而等待的線程。
一般情況下,對於wait/notify/notifyAll方法的調用都是根據一定的條件來進行的,比如:經典的生產者/消費者問題中對於隊列空、滿的判斷。熟悉POSIX的讀者會發現,使用wait/notify/notifyAll可以很容易的實現POSIX中的一個線程間的高級同步技術:條件變量。
簡單例子
本文將圍繞一個簡單的例子展開論述,這樣可以更容易突出我們解決問題的思路、方法。本文想向讀者展現的正是這些思路、方法。這些思路、方法更加適用於解決大規模、復雜應用中的並發問題。考慮一個簡單的例子,我們有一個服務提供者,它通過一個接口對外提供服務,服務內容非常簡單,就是在標準輸出上打印Hello World。類結構圖如下:
代碼如下:
interface Service { public void sayHello(); } class ServiceImp implements Service { public void sayHello() { System.out.println(“Hello World!”); } } class Client { public Client(Service s) { _service = s; } public void requestService() { _service.sayHello(); } private Service _service; } 如果現在有新的需求,要求該服務必須支持Client的並發訪問。一種簡單的方法就是在ServicImp類中的每個方法前面加上synchronized聲明,來保證自己內部數據的一致性(當然對於本例來說,目前是沒有必要的,因為ServiceImp沒有需要保護的數據,但是隨著需求的變化,以後可能會有的)。但是這樣做至少會存在以下幾個問題:
1.現在要維護ServiceImp的兩個版本:多線程版本和單線程版本(有些地方,比如其他項目,可能沒有並發的問題),容易帶來同步更新和正確選擇版本的問題,給維護帶來麻煩。
2.如果多個並發的Client頻繁調用該服務,由於是直接同步調用,會造成Client阻塞,降低服務質量。
3.很難進行一些靈活的控制,比如:根據Client的優先級進行排隊等等。
4.這些問題對於大型的多線程應用服務器尤為突出,對於一些簡單的應用(如本文中的例子)可能根本不用考慮。本文正是要討論這些問題的解決方案,文中的簡單的例子隻是提供瞭一個說明問題,展示思路、方法的平臺。
5.如何才能較好的解決這些問題,有沒有一個可以重用的解決方案呢?讓我們先把這些問題放一放,先來談談和框架有關的一些問題。