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中的一個線程間的高級同步技術
條件變量
有很多的書籍
資料對於synchronized
wait/notify/notifyAll進行了詳細的介紹
參考文獻〔
〕中對於synchronized關鍵字以及和線程有關的Java內存模型有深入詳細的論述
有興趣的讀者可以自行學習
不在此贅述
簡單例子
本文將圍繞一個簡單的例子展開論述
這樣可以更容易突出我們解決問題的思路
方法
本文想向讀者展現的正是這些思路
方法
這些思路
方法更加適用於解決大規模
復雜應用中的並發問題
考慮一個簡單的例子
我們有一個服務提供者
它通過一個接口對外提供服務
服務內容非常簡單
就是在標准輸出上打印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沒有需要保護的數據
但是隨著需求的變化
以後可能會有的)
但是這樣做至少會存在以下幾個問題
現在要維護ServiceImp的兩個版本
多線程版本和單線程版本(有些地方
比如其他項目
可能沒有並發的問題)
容易帶來同步更新和正確選擇版本的問題
給維護帶來麻煩
如果多個並發的Client頻繁調用該服務
由於是直接同步調用
會造成Client阻塞
降低服務質量
很難進行一些靈活的控制
比如
根據Client的優先級進行排隊等等
這些問題對於大型的多線程應用服務器尤為突出
對於一些簡單的應用(如本文中的例子)可能根本不用考慮
本文正是要討論這些問題的解決方案
文中的簡單的例子只是提供了一個說明問題
展示思路
方法的平台
如何才能較好的解決這些問題
有沒有一個可以重用的解決方案呢?讓我們先把這些問題放一放
先來談談和框架有關的一些問題
框架概述
熟悉 面向對象的讀者一定知道面向對象的最大的優勢之一就是
軟件復用
通過復用
可以減少很多的工作量
提高軟件開發生產率
復用本身也是分層次的
代碼級的復用和設計架構的復用
大家可能非常熟悉C語言中的一些標准庫
它們提供了一些通用的功能讓你的程序使用
但是這些標准庫並不能影響你的程序結構和設計思路
僅僅是提供一些機能
幫助你的程序完成工作
它們使你不必重頭編寫一般性的通用功能(比如printf)
它們強調的是程序代碼本身的復用性
而不是設計架構的復用性
那麼什麼是框架呢?所謂框架
它不同於一般的標准庫
是指一組緊密關聯的(類)classes
強調彼此的配合以完成某種可以重復運用的設計概念
這些類之間以特定的方式合作
彼此不可或缺
它們相當程度的影響了你的程序的形貌
框架本身規劃了應用程序的骨干
讓程序遵循一定的流程和動線
展現一定的風貌和功能
這樣就使程序員不必費力於通用性的功能的繁文缛節
集中精力於專業領域
有一點必須要強調
放之四海而皆准的框架是不存在的
也是最沒有用處的
框架往往都是針對某個特定應用領域的
是在對這個應用領域進行深刻理解的基礎上
抽象出該應用的概念模型
在這些抽象的概念上搭建的一個模型
是一個有形無體的框架
不同的具體應用根據自身的特點對框架中的抽象概念進行實現
從而賦予框架生命
完成應用的功能
基於框架的應用都有兩部分構成
框架部分和特定應用部分
要想達到框架復用的目標
必須要做到框架部分和特定應用部分的隔離
使用面向對象的一個強大功能
多態
可以實現這一點
在框架中完成抽象概念之間的交互
關聯
把具體的實現交給特定的應用來完成
其中一般都會大量使用了Template Method設計模式
Java中的Collection Framework以及微軟的MFC都是框架方面很好的例子
有興趣的讀者可以自行研究
構建框架
如何構建一個Java並發模型框架呢?讓我們先回到原來的問題
先來分析一下原因
造成要維護多線程和單線程兩個版本的原因是由於把應用邏輯和並發邏輯混在一起
如果能夠做到把應用邏輯和並發模型進行很好的隔離
那麼應用邏輯本身就可以很好的被復用
而且也很容易把並發邏輯添加進來而不會對應用邏輯造成任何影響
造成Client阻塞
性能降低以及無法進行額外的控制的原因是由於所有的服務調用都是同步的
解決方案很簡單
改為異步調用方式
把服務的調用和服務的執行分離
首先來介紹一個概念
活動對象(Active Object)
所謂活動對象是相對於被動對象(passive object)而言的
被動對象的方法的調用和執行都是在同一個線程中的
被動對象方法的調用是同步的
阻塞的
一般的對象都屬於被動對象
主動對象的方法的調用和執行是分離的
主動對象有自己獨立的執行線程
主動對象的方法的調用是由其他線程發起的
但是方法是在自己的線程中執行的
主動對象方法的調用是異步的
非阻塞的
本框架的核心就是使用主動對象來封裝並發邏輯
然後把Client的請求轉發給實際的服務提供者(應用邏輯)
這樣無論是Client還是實際的服務提供者都不用關心並發的存在
不用考慮並發所帶來的數據一致性問題
從而實現應用邏輯和並發邏輯的隔離
服務調用和服務執行的隔離
下面給出關鍵的實現細節
本框架有如下幾部分構成
一個ActiveObject類
從Thread繼承
封裝了並發邏輯的活動對象
一個ActiveQueue類
主要用來存放調用者請求
一個MethodRequest接口
主要用來封裝調用者的請求
Command設計模式的一種實現方式
它們的一個簡單的實現如下
//MethodRequest接口定義
interface MethodRequest
{
public void call();
}
//ActiveQueue定義
其實就是一個producer/consumer隊列
class ActiveQueue
{
public ActiveQueue() {
_queue = new Stack();
}
public synchronized void enqueue(MethodRequest mr) {
while(_queue
size() > QUEUE_SIZE) {
try {
wait();
}catch (InterruptedException e) {
e
printStackTrace();
}
}
_queue
push(mr);
notifyAll();
System
out
println(
Leave Queue
);
}
public synchronized MethodRequest dequeue() {
MethodRequest mr;
while(_queue
empty()) {
try {
wait();
}catch (InterruptedException e) {
e
printStackTrace();
}
}
mr = (MethodRequest)_queue
pop();
notifyAll();
return mr;
}
private Stack _queue;
pri
From:http://tw.wingwit.com/Article/program/Java/gj/201311/27286.html