Java的多线程特性为构建高性能的应用提供了极大的方便,但是也带来了不少的麻烦。线程间同步、数据一致性等烦琐的问题需要细心的考虑,一不小心就会出现一些微妙的,难以调试的错误。另外,应用逻辑和线程逻辑纠缠在一起,会导致程序的逻辑结构混乱,难以复用和维护。本文试图给出一个解决这个问题的方案,通过构建一个并发模型框架(framework),使得开发多线程的应用变得容易。
<!-- SUMMARY_END -->
基础知识
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没有需要保护的数据,但是随着需求的变化,以后可能会有的)。但是这样做至少会存在以下几个问题:
- 现在要维护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; 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〕中对于构建并发模型框架中的很多细节问题进行了深入的论述,有兴趣的读者可以自行研究。下面列出本框架的优缺点:
优点:
- 增强了应用的并发性,简化了同步控制的复杂性
- 服务的请求和服务的执行分离,使得可以对服务请求排队,进行灵活的控制
- 应用逻辑和并发模型分离,使得程序结构清晰,易于维护、重用
- 可以使开发者集中精力于应用领域
缺点:
- 由于框架所需类的存在,在一定程度上增加了程序的复杂性
- 如果应用需要过多的活动对象,由于线程切换开销会造成性能下降
- 可能会造成调试困难
相关推荐
《Java并发编程实战》是Java并发编程领域的一本经典著作,它深入浅出地介绍了如何在Java平台上进行高效的多线程编程。这本书的源码提供了丰富的示例,可以帮助读者更好地理解书中的理论知识并将其应用到实际项目中。...
书中会首先介绍Java并发编程的基础知识,包括线程的创建和运行,同步机制的基本用法,以及Java内存模型的相关概念。随着章节的深入,作者可能会更深入地讲解Java提供的并发工具,例如锁、原子变量、线程池、以及并发...
《Java并发实战》是一本深度剖析Java并发编程的权威指南,旨在帮助开发者高效地理解和解决多线程环境下的编程挑战。文档清晰明了,配备有详细的目录,使得学习过程更为顺畅,避免了在查找信息上浪费时间。标签“Java...
本文试图给出一个解决这个问题的方案,通过构建一个并发模型框架(framework),使得开发多线程的应用变得容易。基础知识Java语言提供了对于线程很好的支持,实现方法小巧、优雅。对于方法重入的保护,信号量...
Java并发编程是Java语言中最为复杂且重要的部分之一,它涉及了多线程编程、内存模型、同步机制等多个领域。为了深入理解Java并发编程,有必要了解其核心技术点和相关实现原理,以下将详细介绍文件中提及的关键知识点...
第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力 1.2.2 建模的简单性 1.2.3 异步事件的简化处理 1.2.4 响应更灵敏的用户界面 ...第16章 Java内存模型 附录A 并发性标注 参考文献
第三章专门讨论Java内存模型,这是一个对Java并发编程至关重要的主题。Java内存模型定义了程序的不同部分如何共享数据,特别是在多线程环境中。本章详细介绍了内存可见性、原子性和有序性等概念,并探讨了它们如何...
4. **并发集合框架**:Java并发集合框架(java.util.concurrent包)提供了线程安全的数据结构,如ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue等,它们在多线程环境下表现优秀,避免了传统的...
5. **J.U.C框架**:Java并发 utilities (J.U.C) 框架是Java并发编程的重要组成部分,书中会介绍如何利用这个框架来提升并发性能和代码的可读性。 6. **性能调优**:在高并发场景下,性能优化是必不可少的。可能涵盖...
Java并发编程是构建高性能应用的基础,涉及到的知识点非常广泛,包括但不限于线程模型、线程同步、线程间通信以及Java内存模型等方面。掌握这些基础知识,能够帮助开发者更好地理解和解决实际开发中遇到的并发问题,...
它涵盖了Java并发的核心概念、工具和最佳实践,旨在帮助读者在多线程环境中构建高效、可靠的系统。以下是本书涉及的一些关键知识点: 1. **Java并发基础**:介绍Java并发编程的基础知识,包括线程的创建与使用、...
并发模型是指在并发编程中用来描述和处理并发行为的理论框架或抽象模式。常见的并发模型包括: 1. 共享内存模型:线程或进程共享相同的内存地址空间,通过同步机制来协调对共享数据的访问。 2. 消息传递模型:并发...
2. **Java并发API**:详细讲解了Java并发库(java.util.concurrent)中的核心类和接口,如ExecutorService、Future、Semaphore、CountDownLatch、CyclicBarrier、ThreadPoolExecutor等,以及如何使用它们来构建高效...
《Java并发编程高阶技术-高性能并发框架源码解析与实战》是一本深入探讨Java并发编程的书籍,旨在帮助读者掌握高性能并发框架的核心原理,并通过源码解析与实战演练提升技术水平。在Java的世界里,并发编程是提升...
高洪岩先生的《JAVA并发编程 核心方法与框架》深入探讨了这一主题,以下是该主题的一些关键知识点: 1. **线程基础**:线程是操作系统分配CPU时间的基本单位,Java通过`Thread`类提供对线程的支持。创建线程有两种...
特别是ExecutorService,它是Java并发编程框架的核心,可以有效地管理线程池,从而提高系统的资源利用率。 此外,《JAVA并发编程实践》还深入讨论了并发编程的最佳实践,包括避免阻塞、最小化锁的使用、使用并发...
Java 5以及6在开发并发程序取得了显著的进步,提高了Java虚拟机的性能,提高了并发类的可伸缩性,并加入了丰富的新并发构建块。在本书中,这些便利工具的创造者不仅解释了它们究竟如何工作、如何使用,同时,还阐释...
Java并发编程是Java开发语言中的一个关键领域,特别是在构建高性能、高并发的后端系统时。Apache作为流行的开源软件框架,提供了许多与并发处理相关的工具和库,使得开发者能够更有效地管理和优化多线程环境。本教程...