2002 年 2 月 22 日

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进行了详细的介绍,参考文献〔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. 可能会造成调试困难

参考资料

[1] Active Object An Object Behavioral Pattern for Concurrent, Schmidt, Douglass, Lavender, R. Greg, Programming, http://www.cs.wustl.edu/~schmidt/PDF/Act-Obj.pdf

[2] Effective Java Programming Language Guide, Joshua Bloch

[3] Double-checked locking: Clever, but broken, Brian Goetz, http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-double.htm

[4] Design Patterns, Gamma, et. al., Addison Wesley

转载于:https://www.cnblogs.com/licheng/archive/2008/09/23/1296829.html

构建Java并发模型框架相关推荐

  1. Java并发基础框架AbstractQueuedSynchronizer初探(ReentrantLock的实现分析)

    AbstractQueuedSynchronizer是实现Java并发类库的一个基础框架,Java中的各种锁(RenentrantLock, ReentrantReadWriteLock)以及同步工具 ...

  2. java 并发模型总类_java并发编程系列-内存模型基础

    java线程之间的通信对程序开发人员是完全透明的,内存的可见性问题很容易困扰很多开发人员.本篇博文将揭开java内存模型的神秘面纱,来看看内存模型到底是怎样的. 并发编程模型的分类 并发编程中需要处理 ...

  3. java源文件编译成jar_从源文件和JAR文件构建Java代码模型

    java源文件编译成jar 最近,我花了一些时间来研究有效java ,该方法正在GitHub上达到300星(可以免费帮助实现目标:D). Effectivejava是在您的Java代码上运行查询的工具 ...

  4. 从源文件和JAR文件构建Java代码模型

    最近,我花了一些时间来研究有效java ,该方法正在GitHub上达到300星(随时帮助实现目标:D). Effectivejava是在您的Java代码上运行查询的工具. 它基于我参与的另一个项目ja ...

  5. java 并发编程框架

    java 并发编程 线程组管理 Executor 框架 Fork-Join框架 线程组管理 线程组: 线程的集合 大线程可包含小线程. 可管理多个线程,但效率较低 重复的创建线程和关闭线程 可通过en ...

  6. Java并发编程框架Disruptor

    Disruptor是什么? Disruptor是一个高性能的异步处理框架,一个轻量级的JMS,和JDK中的BlockingQueue有相似处,但是它的处理速度非常快,获得2011年程序框架创新大奖,号 ...

  7. Java事件驱动模型框架实现

    一.框架实现篇 关键角色: 事件源(XXXEvent):能够接收外部事件的源体,内部包含事件的类型. 事件监听器(XXXEventListener):能够接收事件源通知的对象. 事件分发器(Event ...

  8. Java并发——Executor框架详解(Executor框架结构与框架成员)

    一.什么是Executor框架? 我们知道线程池就是线程的集合,线程池集中管理线程,以实现线程的重用,降低资源消耗,提高响应速度等.线程用于执行异步任务,单个的线程既是工作单元也是执行机制,从JDK1 ...

  9. java并发集合框架学习记录

    1. concurrentHashMap图解:https://mp.weixin.qq.com/s?__biz=MzI1MTIzMzI2MA==&mid=2650561829&idx= ...

最新文章

  1. OpenCV像素点处理
  2. QQ互联OAuth2.0 .NET SDK 发布以及网站QQ登陆示例代码
  3. qt 实现的电视遥控系统,如何让qt响应来自遥控器的按键信息?
  4. 关于ABAP流程处理的一些命令的说明(stop,exit,return,check,reject)
  5. 分布式与人工智能课程(part8)--Pandas绘图
  6. 倩女幽魂7月20日服务器维护,倩女幽魂手游7月20日更新什么?倩女幽魂手游7月20日更新一览...
  7. 数据资产标准研究进展与建议
  8. 在 Linux 中安装 VMware Tools
  9. 如何上好中职生的计算机课,如何上好中职生计算机基础课.doc
  10. 死锁问题------------------------INSERT ... ON DUPLICATE KEY UPDATE*(转)
  11. CANFD和CAN的区别简介
  12. c语言的缺陷与陷阱,C语言 宏陷阱与缺陷
  13. 【图形图像处理】之栅格化介绍
  14. hanlp 如何快速从分词仅取出人名
  15. oracle中导出查询结果,Oracle SQL 查询结果导出到文本或Excel文件方法
  16. 笔记本光驱未连接在计算机上,笔记本电脑光驱,详细教您笔记本光驱怎么打开...
  17. 什么是Race Condition?
  18. 极验验证码逆向(一)
  19. Android 玩机之路(刷第三方recovery twrp+面具Magisk)
  20. APP定制开发的几种付费模式

热门文章

  1. mac下安装libpng环境
  2. IOS中CoreLocation框架地理定位
  3. ZendFramework的介绍、安装和实例运行
  4. 生成静态文件的新闻系统核心代码
  5. PHP中Session的使用
  6. Linux内存管理大图(第三稿)
  7. C++中std::sort/std::stable_sort/std::partial_sort的区别及使用
  8. 【linux】NXP MFGTools工具配置文件详解
  9. oracle web API,在Web API程序中使用Swagger做接口文档
  10. ednote服务器运行失败,endnote搜索文献时连不上服务器 Endnote连接服务器问题