构建自己的Java并发模型框架

Java的多线程特性为构建高性能的应用提供了极大的方便,可是也带来了不少的麻烦。线程间同步、数据一致性等烦琐的问题须要细心的考虑,一不小心就会出现一些微妙的,难以调试的错误。

另外。应用逻辑和线程逻辑纠缠在一起。会导致程序的逻辑结构混乱,难以复用和维护。

本文试图给出一个解决问题的方案。通过构建一个并发模型框架(framework),使得开发多线程的应用变得easy。

基础知识

Java中内置了对于对象并发訪问的支持,每个对象都有一个监视器(monitor)。同一时候仅仅同意一个线程持有监视器从而进行对对象的訪问,那些没有获得监视器的线程必须等待直到持有监视器的线程释放监视器。对象通过synchronizedkeyword来声明线程必须获得监视器才干进行对自己的訪问。

synchronized声明只对于一些较为简单的线程间同步问题比較有效,对于哪些复杂的同步问题。比方带有条件的同步问题,Java提供了另外的解决方法,wait/notify/notifyAll。获得对象监视器的线程能够通过调用该对象的wait方法主动释放监视器,等待在该对象的线程等待队列上,此时其它线程能够得到监视器从而訪问该对象,之后能够通过调用notify/notifyAll方法来唤醒先前因调用wait方法而等待的线程。

普通情况下,对于wait/notify/notifyAll方法的调用都是依据一定的条件来进行的。比方:经典的生产者/消费者问题中对于队列空、满的推断。熟悉POSIX的读者会发现,使用wait/notify/notifyAll能够非常easy的实现POSIX中的一个线程间的高级同步技术:条件变量。


简单样例

考虑一个简单的样例,我们有一个服务提供者。它通过一个接口对外提供服务,服务内容很easy,就是在标准输出上打印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的两个版本号:多线程版本号和单线程版本号(有些地方,比方其它项目,可能没有并发的问题)。easy带来同步更新和正确选择版本号的问题,给维护带来麻烦。
  2. 假设多个并发的Client频繁调用该服务。因为是直接同步调用,会造成Client堵塞,减少服务质量。
  3. 非常难进行一些灵活的控制,比方:依据Client的优先级进行排队等等。

这些问题对于大型的多线程应用server尤为突出,对于一些简单的应用(如本文中的样例)可能根本不用考虑。本文正是要讨论这些问题的解决方式。文中的简单的样例仅仅是提供了一个说明问题。展示思路、方法的平台。

怎样才干较好的解决这些问题,有没有一个能够重用的解决方式呢?让我们先把这些问题放一放,先来谈谈和框架有关的一些问题。



框架概述

熟悉面向对象的读者一定知道面向对象的最大的优势之中的一个就是:软件复用。通过复用,能够降低非常多的工作量。提高软件开发生产率。复用本身也是分层次的。代码级的复用和设计架构的复用。

那么什么是框架呢?所谓框架,它不同于一般的标准库,是指一组紧密关联的(类)classes,强调彼此的配合以完毕某种能够反复运用的设计概念。这些类之间以特定的方式合作。彼此不可或缺。

它们相当程度的影响了你的程序的形貌。框架本身规划了应用程序的骨干,让程序遵循一定的流程和动线。展现一定的风貌和功能。

这样就使程序猿不必费力于通用性的功能的繁文缛节,集中精力于专业领域。
有一点必需要强调,放之四海而皆准的框架是不存在的。也是最没实用处的。框架往往都是针对某个特定应用领域的。是在对这个应用领域进行深刻理解的基础上,抽象出该应用的概念模型。在这些抽象的概念上搭建的一个模型,是一个有形无体的框架。不同的详细应用依据自身的特点对框架中的抽象概念进行实现,从而赋予框架生命。完毕应用的功能。
基于框架的应用都有两部分构成:框架部分和特定应用部分。要想达到框架复用的目标,必需要做到框架部分和特定应用部分的隔离。

使用面向对象的一个强大功能:多态。能够实现这一点。在框架中完毕抽象概念之间的交互、关联,把详细的实现交给特定的应用来完毕。

当中一般都会大量使用了Template Method设计模式。


构建框架

怎样构建一个Java并发模型框架呢?让我们先回到原来的问题,先来分析一下原因。造成要维护多线程和单线程两个版本号的原因是因为把应用逻辑和并发逻辑混在一起,假设可以做到把应用逻辑和并发模型进行非常好的隔离。那么应用逻辑本身就行非常好的被复用,并且也非常容易把并发逻辑加入进来而不会相应用逻辑造成不论什么影响。造成Client堵塞。性能减少以及无法进行额外的控制的原因是因为全部的服务调用都是同步的,解决方式非常easy。改为异步调用方式,把服务的调用和服务的运行分离。
首先来介绍一个概念,活动对象(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;
    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 ServiceImp();
Client c = new Client(s);
c.requestService();

可以看出并发逻辑添加前后对于Client的ServiceImp都无需作不论什么改变,使用方式也很一致,ServiceImp也可以独立的进行重用。类结构图例如以下:


读者easy看出,使用框架也添加了一些复杂性。对于一些简单的应用来说可能根本就没有必要使用本框架。

希望读者可以依据自己的实际情况进行推断。


结论

本文环绕一个简单的样例论述了怎样构架一个Java并发模型框架,当中使用了一些构建框架的经常使用技术。当然所构建的框架和一些成熟的商用框架相比,显得非常稚嫩。比方没有考虑服务调用有返回值的情况,可是其思想方法是一致的,希望读者能够深加领会,这样不管对于构建自己的框架还是理解一些其它的框架都是非常有帮助的。

读者能够对本文中的框架进行扩充,直接应用到自己的工作中。參考文献〔1〕中对于构建并发模型框架中的非常多细节问题进行了深入的论述。有兴趣的读者能够自行研究。以下列出本框架的优缺点:
长处:
增强了应用的并发性,简化了同步控制的复杂性
服务的请求和服务的运行分离,使得能够对服务请求排队,进行灵活的控制
应用逻辑和并发模型分离,使得程序结构清晰。易于维护、重用
能够使开发人员集中精力于应用领域
缺点:
因为框架所需类的存在,在一定程度上添加了程序的复杂性
假设应用须要过多的活动对象,因为线程切换开销会造成性能下降
可能会造成调试困难

原文地址:https://www.cnblogs.com/bhlsheji/p/5088396.html