天天看點

建構Java并發模型架構

ava的多線程特性為建構高性能的應用提供了極大的友善,但是也帶來了不少的麻煩。線程間同步、資料一緻性等煩瑣的問題需要細心的考慮,一不小心就會出現一些微妙的,難以調試的錯誤。另外,應用邏輯和線程邏輯糾纏在一起,會導緻程式的邏輯結構混亂,難以複用和維護。本文試圖給出一個解決這個問題的方案,通過建構一個并發模型架構(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進行了詳細的介紹,參考文獻〔3〕中對于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沒有需要保護的資料,但是随着需求的變化,以後可能會有的)。但是這樣做至少會存在以下幾個問題:

  1. 現在要維護ServiceImp的兩個版本:多線程版本和單線程版本(有些地方,比如其他項目,可能沒有并發的問題),容易帶來同步更新和正确選擇版本的問題,給維護帶來麻煩。
  2. 如果多個并發的Client頻繁調用該服務,由于是直接同步調用,會造成Client阻塞,降低服務品質。
  3. 很難進行一些靈活的控制,比如:根據Client的優先級進行排隊等等。

這些問題對于大型的多線程應用伺服器尤為突出,對于一些簡單的應用(如本文中的例子)可能根本不用考慮。本文正是要讨論這些問題的解決方案,文中的簡單的例子隻是提供了一個說明問題,展示思路、方法的平台。

如何才能較好的解決這些問題,有沒有一個可以重用的解決方案呢?讓我們先把這些問題放一放,先來談談和架構有關的一些問題。

架構概述

熟悉 面向對象的讀者一定知道面向對象的最大的優勢之一就是:軟體複用。通過複用,可以減少很多的工作量,提高軟體開發生産率。複用本身也是分層次的,代碼級的複用和設計架構的複用。

大家可能非常熟悉C語言中的一些标準庫,它們提供了一些通用的功能讓你的程式使用。但是這些标準庫并不能影響你的程式結構和設計思路,僅僅是提供一些機能,幫助你的程式完成工作。它們使你不必重頭編寫一般性的通用功能(比如printf),它們強調的是程式代碼本身的複用性,而不是設計架構的複用性。

那麼什麼是架構呢?所謂架構,它不同于一般的标準庫,是指一組緊密關聯的(類)classes,強調彼此的配合以完成某種可以重複運用的設計概念。這些類之間以特定的方式合作,彼此不可或缺。它們相當程度的影響了你的程式的形貌。架構本身規劃了應用程式的骨幹,讓程式遵循一定的流程和動線,展現一定的風貌和功能。這樣就使程式員不必費力于通用性的功能的繁文缛節,集中精力于專業領域。

有一點必須要強調,放之四海而皆準的架構是不存在的,也是最沒有用處的。架構往往都是針對某個特定應用領域的,是在對這個應用領域進行深刻了解的基礎上,抽象出該應用的概念模型,在這些抽象的概念上搭建的一個模型,是一個有形無體的架構。不同的具體應用根據自身的特點對架構中的抽象概念進行實作,進而賦予架構生命,完成應用的功能。

基于架構的應用都有兩部分構成:架構部分和特定應用部分。要想達到架構複用的目标,必須要做到架構部分和特定應用部分的隔離。使用面向對象的一個強大功能:多态,可以實作這一點。在架構中完成抽象概念之間的互動、關聯,把具體的實作交給特定的應用來完成。其中一般都會大量使用了Template Method設計模式。

Java中的Collection Framework以及微軟的MFC都是架構方面很好的例子。有興趣的讀者可以自行研究。

建構架構

如何建構一個Java并發模型架構呢?讓我們先回到原來的問題,先來分析一下原因。造成要維護多線程和單線程兩個版本的原因是由于把應用邏輯和并發邏輯混在一起,如果能夠做到把應用邏輯和并發模型進行很好的隔離,那麼應用邏輯本身就可以很好的被複用,而且也很容易把并發邏輯添加進來而不會對應用邏輯造成任何影響。造成Client阻塞,性能降低以及無法進行額外的控制的原因是由于所有的服務調用都是同步的,解決方案很簡單,改為異步調用方式,把服務的調用和服務的執行分離。

首先來介紹一個概念,活動對象(Active Object)。所謂活動對象是相對于被動對象(passive object)而言的,被動對象的方法的調用和執行都是在同一個線程中的,被動對象方法的調用是同步的、阻塞的,一般的對象都屬于被動對象;主動對象的方法的調用和執行是分離的,主動對象有自己獨立的執行線程,主動對象的方法的調用是由其他線程發起的,但是方法是在自己的線程中執行的,主動對象方法的調用是異步的,非阻塞的。

本架構的核心就是使用主動對象來封裝并發邏輯,然後把Client的請求轉發給實際的服務提供者(應用邏輯),這樣無論是Client還是實際的服務提供者都不用關心并發的存在,不用考慮并發所帶來的資料一緻性問題。進而實作應用邏輯和并發邏輯的隔離,服務調用和服務執行的隔離。下面給出關鍵的實作細節。

本架構有如下幾部分構成:

  1. 一個ActiveObject類,從Thread繼承,封裝了并發邏輯的活動對象
  2. 一個ActiveQueue類,主要用來存放調用者請求
  3. 一個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;
    private final static int QUEUE_SIZE = 20;
}
//ActiveObject的定義
class ActiveObject extends Thread
{
    public ActiveObject() {
        _queue = new ActiveQueue();
        start();
    }
    public void enqueue(MethodRequest mr) {
        _queue.enqueue(mr);
    }
    public void run() {
        while(true) {
            MethodRequest mr = _queue.dequeue();
            mr.call();
        }
    } 
    private ActiveQueue _queue;
}      

通過上面的代碼可以看出正是這些類互相合作完成了對并發邏輯的封裝。開發者隻需要根據需要實作MethodRequest接口,另外再定義一個服務代理類提供給使用者,在服務代理者類中把服務調用者的請求轉化為MethodRequest實作,交給活動對象即可。

使用該架構,可以較好的做到應用邏輯和并發模型的分離,進而使開發者集中精力于應用領域,然後平滑的和并發模型結合起來,并且可以針對ActiveQueue定制排隊機制,比如基于優先級等。

基于架構的解決方案

本小節将使用上述的架構重新實作前面的例子,提供對于并發的支援。第一步先完成對于MethodRequest的實作,對于我們的例子來說實作如下:

class SayHello implements MethodRequest
{
    public SayHello(Service s) {
        _service = s;
    }
    public void call() {
        _service.sayHello();
    }
    private Service _service;
}      

該類完成了對于服務提供接口sayHello方法的封裝。接下來定義一個服務代理類,來完成請求的封裝、排隊功能,當然為了做到對Client透明,該類必須實作Service接口。定義如下:

class ServiceProxy implements Service
{
    public ServiceProxy() {
        _service = new ServiceImp();
        _active_object = new ActiveObject();
    }
    
    public void sayHello() {
        MethodRequest mr = new SayHello(_service);
        _active_object.enqueue(mr);
    }
    private Service _service;
    private ActiveObject _active_object;
}      

其他的類和接口定義不變,下面對比一下并發邏輯增加前後的服務調用的變化,并發邏輯增加前,對于sayHello服務的調用方法:

Service s = new ServiceImp();
       Client c = new Client(s);
       c.requestService();      

并發邏輯增加後,對于sayHello服務的調用方法:

Service s = new  ServiceProxy();
      Client c = new Client(s);
        c.requestService();      

可以看出并發邏輯增加前後對于Client的ServiceImp都無需作任何改變,使用方式也非常一緻,ServiceImp也能夠獨立的進行重用。類結構圖如下:

讀者容易看出,使用架構也增加了一些複雜性,對于一些簡單的應用來說可能根本就沒有必要使用本架構。希望讀者能夠根據自己的實際情況進行判斷。

結論

本文圍繞一個簡單的例子論述了如何構架一個Java并發模型架構,其中使用了一些建構架構的常用技術,當然所建構的架構和一些成熟的商用架構相比,顯得非常稚嫩,比如沒有考慮服務調用有傳回值的情況,但是其思想方法是一緻的,希望讀者能夠深加領會,這樣無論對于建構自己的架構還是了解一些其他的架構都是很有幫助的。讀者可以對本文中的架構進行擴充,直接應用到自己的工作中。參考文獻〔1〕中對于建構并發模型架構中的很多細節問題進行了深入的論述,有興趣的讀者可以自行研究。下面列出本架構的優缺點:

優點:

  1. 增強了應用的并發性,簡化了同步控制的複雜性
  2. 服務的請求和服務的執行分離,使得可以對服務請求排隊,進行靈活的控制
  3. 應用邏輯和并發模型分離,使得程式結構清晰,易于維護、重用
  4. 可以使開發者集中精力于應用領域
  1. 由于架構所需類的存在,在一定程度上增加了程式的複雜性
  2. 如果應用需要過多的活動對象,由于線程切換開銷會造成性能下降
  3. 可能會造成調試困難