`
dchaoxiong
  • 浏览: 183430 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Java并发使一切变得简单—util.concurrent 包简介

阅读更多

内容:  

  探究重复发明“车轮”之原因

  并发构件

  调度异步任务

  Executor

  FutureResult

  结束语

  参考资料

  关于作者

  对本文的评价



  对于每个项目,象许多其它应用程序基础结构服务一样,通常无需从头重新编写并发实用程序类(如工作队列和线程池)。这个月,Brian Goetz 将介绍 Doug Lea 的 util.concurrent 包,这是一个高质量的、广泛使用的、并发实用程序的开放源码包。

  当项目中需要 XML 解析器、文本索引程序和搜索引擎、正则表达式编译器、XSL 处理器或 PDF 生成器时,我们中大多数人从不会考虑自己去编写这些实用程序。每当需要这些设施时,我们会使用商业实现或开放源码实现来执行这些任务原因很简单 — 现有实现工作得很好,而且易于使用,自己编写这些实用程序会事倍功半,或者甚至得不到结果。作为软件工程师,我们更愿意遵循艾萨克·牛顿的信念 — 站在巨人的肩膀之上,有时这是可取的,但并不总是这样。(在 Richard Hamming 的 Turing Award 讲座中,他认为计算机科学家的“自立”要更可取。)

  探究重复发明“车轮”之原因

  对于一些几乎每个服务器应用程序都需要的低级应用程序框架服务(如日志记录、数据库连接合用、高速缓存和任务调度等),我们看到这些基本的基础结构服务被一遍又一遍地重写。为什么会发生这种情况?因为现有的选择不够充分,或者因为定制版本要更好些或更适合手边的应用程序,但我认为这是不必要的。事实上,专为某个应用程序开发的定制版本常常并不比广泛可用的、通用的实现更适合于该应用程序,也许会更差。例如,尽管您不喜欢 log4j,但它可以完成任务。尽管自己开发的日志记录系统也许有一些 log4j 所缺乏的特定特性,但对于大多数应用程序,您很难证明,一个完善的定制日志记录包值得付出从头编写的代价,而不使用现有的、通用的实现。可是,许多项目团队最终还是自己一遍又一遍地编写日志记录、连接合用或线程调度包。

  表面上看起来简单

  我们不考虑自己去编写 XSL 处理器的原因之一是,这将花费大量的工作。但这些低级的框架服务表面上看起来简单,所以自己编写它们似乎并不困难。然而,它们很难正常工作,并不象开始看起来那样。这些特殊的“轮子”一直处在重复发明之中的主要原因是,在给定的应用程序中,往往一开始对这些工具的需求非常小,但当您遇到了无数其它项目中也存在的同样问题时,这种需求会逐渐变大。理由通常象这样:“我们不需要完善的日志记录/调度/高速缓存包,只需要一些简单的包,所以只编写一些能达到我们目的的包,我们将针对自己特定的需求来调整它”。但情况往往是,您很快扩展了所编写的这个简单工具,并试图添加再添加更多的特性,直到编写出一个完善的基础结构服务。至此,您通常会执著于自己所编写的程序,无论它是好是坏。您已经为构建自己的程序付出了全部的代价,所以除了转至通用的实现所实际投入的迁移成本之外,还必须克服这种“已支付成本”的障碍。

  并发构件的价值所在

  编写调度和并发基础结构类的确要比看上去难。Java 语言提供了一组有用的低级同步原语:wait()、 notify() 和 synchronized,但具体使用这些原语需要一些技巧,需要考虑性能、死锁、公平性、资源管理以及如何避免线程安全性方面带来的危害等诸多因素。并发代码难以编写,更难以测试 — 即使专家有时在第一次时也会出现错误。Concurrent Programming in Java(请参阅参考资料)的作者 Doug Lea 编写了一个极其优秀的、免费的并发实用程序包,它包括并发应用程序的锁、互斥、队列、线程池、轻量级任务、有效的并发集合、原子的算术操作和其它基本构件。人们一般称这个包为 util.concurrent(因为它实际的包名很长),该包将形成 Java Community Process JSR 166 正在标准化的 JDK 1.5 中 java.util.concurrent 包的基础。同时,util.concurrent 经过了良好的测试,许多服务器应用程序(包括 JBoss J2EE 应用程序服务器)都使用这个包。

  填补空白

  核心 Java 类库中略去了一组有用的高级同步工具(譬如互斥、信号和阻塞、线程安全集合类)。Java 语言的并发原语 — synchronization、wait() 和 notify() — 对于大多数服务器应用程序的需求而言过于低级。如果要试图获取锁,但如果在给定的时间段内超时了还没有获得它,会发生什么情况?如果线程中断了,则放弃获取锁的尝试?创建一个至多可有 N 个线程持有的锁?支持多种方式的锁定(譬如带互斥写的并发读)?或者以一种方式来获取锁,但以另一种方式释放它?内置的锁定机制不直接支持上述这些情形,但可以在 Java 语言所提供的基本并发原语上构建它们。但是这样做需要一些技巧,而且容易出错。

  服务器应用程序开发人员需要简单的设施来执行互斥、同步事件响应、跨活动的数据通信以及异步地调度任务。对于这些任务,Java 语言所提供的低级原语很难用,而且容易出错。util.concurrent 包的目的在于通过提供一组用于锁定、阻塞队列和任务调度的类来填补这项空白,从而能够处理一些常见的错误情况或者限制任务队列和运行中的任务所消耗的资源。

  调度异步任务

  util.concurrent 中使用最广泛的类是那些处理异步事件调度的类。在本专栏七月份的文章中,我们研究了 thread pools and work queues,以及许多 Java 应用程序是如何使用“Runnable 队列”模式调度小工作单元。

  可以通过简单地为某个任务创建一个新线程来派生执行该任务的后端线程,这种做法很吸引人:

new Thread(new Runnable() { ... } ).start();


  虽然这种做法很好,而且很简洁,但有两个重大缺陷。首先,创建新的线程需要耗费一定资源,因此产生出许许多多线程,每个将执行一个简短的任务,然后退出,这意味着 JVM 也许要做更多的工作,创建和销毁线程而消耗的资源比实际做有用工作所消耗的资源要多。即使创建和销毁线程的开销为零,这种执行模式仍然有第二个更难以解决的缺陷 — 在执行某类任务时,如何限制所使用的资源?如果突然到来大量的请求,如何防止同时会产生大量的线程?现实世界中的服务器应用程序需要比这更小心地管理资源。您需要限制同时执行异步任务的数目。

  线程池解决了以上两个问题 — 线程池具有可以同时提高调度效率和限制资源使用的好处。虽然人们可以方便地编写工作队列和用池线程执行 Runnable 的线程池(七月份那篇专栏文章中的示例代码正是用于此目的),但编写有效的任务调度程序需要做比简单地同步对共享队列的访问更多的工作。现实世界中的任务调度程序应该可以处理死线程,杀死超量的池线程,使它们不消耗不必要的资源,根据负载动态地管理池的大小,以及限制排队任务的数目。为了防止服务器应用程序在过载时由于内存不足错误而造成崩溃,最后一项(即限制排队的任务数目)是很重要的。

  限制任务队列需要做决策 — 如果工作队列溢出,则如何处理这种溢出?抛弃最新的任务?抛弃最老的任务?阻塞正在提交的线程直到队列有可用的空间?在正在提交的线程内执行新的任务?存在着各种切实可行的溢出管理策略,每种策略都会在某些情形下适合,而在另一些情形下不适合。

  Executor

  Util.concurrent 定义一个 Executor 接口,以异步地执行 Runnable,另外还定义了 Executor 的几个实现,它们具有不同的调度特征。将一个任务排入 executor 的队列非常简单:

Executor executor = new QueuedExecutor();
...
Runnable runnable = ... ;
executor.execute(runnable);


  最简单的实现 ThreadedExecutor 为每个 Runnable 创建了一个新线程,这里没有提供资源管理 — 很象 new Thread(new Runnable() {}).start() 这个常用的方法。但 ThreadedExecutor 有一个重要的好处:通过只改变 executor 结构,就可以转移到其它执行模型,而不必缓慢地在整个应用程序源码内查找所有创建新线程的地方。QueuedExecutor 使用一个后端线程来处理所有任务,这非常类似于 AWT 和 Swing 中的事件线程。QueuedExecutor 具有一个很好的特性:任务按照排队的顺序来执行,因为是在一个线程内来执行所有的任务,任务无需同步对共享数据的所有访问。

  PooledExecutor 是一个复杂的线程池实现,它不但提供工作线程(worker thread)池中任务的调度,而且还可灵活地调整池的大小,同时还提供了线程生命周期管理,这个实现可以限制工作队列中任务的数目,以防止队列中的任务耗尽所有可用内存,另外还提供了多种可用的关闭和饱和度策略(阻塞、废弃、抛出、废弃最老的、在调用者中运行等)。所有的 Executor 实现为您管理线程的创建和销毁,包括当关闭 executor 时,关闭所有线程,另外还为线程创建过程提供了 hook,以便应用程序可以管理它希望管理的线程实例化。例如,这使您可以将所有工作线程放在特定的 ThreadGroup 中,或者赋予它们描述性名称。

  FutureResult

  有时您希望异步地启动一个进程,同时希望在以后需要这个进程时,可以使用该进程的结果。FutureResult 实用程序类使这变得很容易。FutureResult 表示可能要花一段时间执行的任务,并且可以在另一个线程中执行此任务,FutureResult 对象可用作执行进程的句柄。通过它,您可以查明该任务是否已经完成,可以等待任务完成,并检索其结果。可以将 FutureResult 与 Executor 组合起来;可以创建一个 FutureResult 并将其排入 executor 的队列,同时保留对 FutureResult 的引用。清单 1 显示了一个一同使用 FutureResult 和 Executor 的简单示例,它异步地启动图像着色,并继续进行其它处理:

  清单 1. 运作中的 FutureResult 和 Executor Executor executor = ...

ImageRenderer renderer = ...

       FutureResult futureImage = new FutureResult();
       Runnable command = futureImage.setter(new Callable() {
          public Object call() { return renderer.render(rawImage); }
       });

    // start the rendering process
       executor.execute(command);

       // do other things while executing
       drawBorders();
       drawCaption();

    // retrieve the future result, blocking if necessary
       drawImage((Image)(futureImage.get())); // use future


  FutureResult 和高速缓存

  还可以使用 FutureResult 来提高按需装入高速缓存的并发性。通过将 FutureResult 放置在高速缓存内,而不是放置计算本身的结果,可以减少持有高速缓存上写锁的时间。虽然这种做法不能加快第一个线程把某一项放入高速缓存,但它将减少第一个线程阻塞其它线程访问高速缓存的时间。它还使其它线程更早地使用结果,因为它们可以从高速缓存中检索 FutureTask。清单 2 显示了使用用于高速缓存的 FutureResult 示例:

  清单 2. 使用 FutureResult 来改善高速缓存 public class FileCache {

private Map cache = new HashMap();
  private Executor executor = new PooledExecutor();

  public void get(final String name) {
    FutureResult result;

    synchronized(cache) {
      result = cache.get(name);
      if (result == null) {
        result = new FutureResult();
        executor.execute(result.setter(new Callable() {
          public Object call() { return loadFile(name); }
        }));
        cache.put(result);
      }
    }
    return result.get();
  }
}


  这种方法使第一个线程快速地进入和退出同步块,使其它线程与第一个线程一样快地得到第一个线程计算的结果,不可能出现两个线程都试图计算同一个对象。

  结束语

  util.concurrent 包包含许多有用的类,您可能认为其中一些类与您已编写的类一样好,也许甚至比从前还要好。它们是许多多线程应用程序的基本构件的高性能实现,并经历了大量测试。util.concurrent 是 JSR 166 的切入点,它将带来一组并发性的实用程序,这些实用程序将成为 JDK 1.5 中的 java.util.concurrent 包,但您不必等到那时侯才能使用它。在以后的文章中,我将讨论 util.concurrent 中一些定制的同步类,并研究 util.concurrent 和 java.util.concurrent API 中的不同之处。

分享到:
评论

相关推荐

    java.util.concurrent 实现线程池队列

    Java并发编程是Java开发中的重要组成部分,特别是在多核处理器和高并发应用环境下,高效地管理线程资源变得至关重要。`java.util.concurrent` 包(简称JUC)是Java提供的一个强大的并发工具包,它提供了丰富的并发...

    java.util.concurrent_您不知道的5件事

    `java.util.concurrent` 包中的这些同步工具为开发者提供了强大的并发控制手段,使得多线程编程变得更加容易管理。通过合理利用这些工具,开发者可以在保证程序正确性的同时,提高程序的性能和响应速度。无论是简单...

    java.util.concurrent介绍(重要).pdf

    总之,`java.util.concurrent` 提供的工具使得并发编程变得更加容易和高效,是 Java 并发编程的基石,无论是对于初学者还是经验丰富的开发者,理解和掌握这个包都是非常重要的。通过熟练运用这些工具,开发者可以...

    (PDF带目录)《Java 并发编程实战》,java并发实战,并发

    4. **并发工具类**:`java.util.concurrent`包还包含一系列并发工具类,如`ExecutorService`、`Future`、`Callable`等,它们简化了任务的提交、管理和结果获取,使并发编程更加灵活和高效。 5. **原子变量**:Java...

    JUC-High-Concurrency:java.util.concurrent JUC高并发处理

    Java.util.concurrent(JUC)是Java平台中处理高并发的核心工具包,它为开发者提供了丰富的并发组件和机制,使得在多线程环境下的编程变得更加高效和安全。本篇将深入探讨JUC中的重要知识点,包括同步、可重入锁、类...

    Java并发编程实践源码

    通过阅读和分析这些代码,我们可以深入理解Java并发API的用法,如`java.util.concurrent`包下的工具类和接口。 2. **ListenerExamples.java**:此文件可能涉及到事件驱动编程和监听器模式,这是GUI编程中常见的并发...

    [Java并发编程实践].(Java.Concurrency.in.Practice).Brian.Goetz.文字版

    - **Cliff Click**(Azul Systems资深软件工程师)提到随着摩尔定律的变化趋势,有效地利用并发变得越来越重要,本书将教会读者如何做到这一点。 通过以上内容可以看出,《Java并发编程实践》不仅深入浅出地讲解了...

    Java并发编程实践 全集

    Java提供了丰富的并发工具和API,如`Thread`类、`ExecutorService`、`Future`、`synchronized`关键字、`volatile`、`java.util.concurrent`包等,这些工具使得并发编程变得更加安全和易于管理。 1. **线程基础**:...

    java并发编程和网络高级编程pdf

    Java在网络编程方面提供了丰富的API,使得开发网络应用变得简单。《Java网络高级编程》可能涵盖以下知识点: 1. **套接字编程**:包括`Socket`和`ServerSocket`类,用于实现客户端-服务器通信。 2. **多路复用I/O**...

    java多线程编程总结

    Java5之前的版本虽然支持多线程,但在语言层面的支持较为有限,这使得编写复杂的多线程程序变得非常具有挑战性。然而,随着Java5及后续版本的发布,Java引入了一系列强大的多线程特性和工具,极大地增强了多线程编程...

    Concurrent Programming in Java™: Design Principles and Patterns 2nd

    3. **可移植并行编程**:随着多核处理器的普及,编写能够有效利用这些资源的并行程序变得越来越重要。本书介绍了如何使用Java编写可移植的并行代码。 4. **并发控制的工具类**:Java提供了多种工具类来帮助管理并发...

    Java并发编程与高并发解决方案.txt

    Java.util.concurrent(简称J.U.C)是Java并发编程的核心库之一,提供了大量的并发容器和原子变量类,以及各种同步工具类。它简化了并发编程中的许多复杂细节,提高了开发效率和程序的稳定性。 ##### 4.2 原子类 J...

    Java并发编程实战(中文版).rar

    并发编程是现代软件开发中的重要组成部分,特别是在Java平台中,由于其内置的并发API和强大的JVM支持,使得并发处理变得既强大又复杂。本书的核心目标是教授读者如何有效地编写高效、安全和可维护的并发代码。 首先...

    Java并发程序设计教程

    Java并发程序设计是Java开发中的重要领域,尤其在多核处理器和分布式系统广泛使用的今天,理解和掌握并发编程技术显得尤为重要。本教程将深入探讨Java语言...通过实践和持续学习,你将在Java并发编程领域变得更加专业。

    JAVA课件 swing io util

    `java.util.concurrent`包则提供了并发编程的支持,包括高级的线程池、同步容器等。`java.io.Util`虽然不如其他两个常见,但其中有一些辅助I/O操作的工具,如copy()方法用于文件复制。 在实际开发中,Swing、IO和...

    javaapi.zip

    3. **多线程**:Java 支持多线程编程,通过 `java.lang.Thread` 类和 `java.util.concurrent` 包,开发者可以创建和管理并发任务。`java.util.concurrent` 包提供了一些高级并发工具,如 Executor、Semaphore 和 ...

    JAVA定时任务

    Spring框架的`@Scheduled`注解则让定时任务的配置变得简单,例如: ```java import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public ...

    java并发编程实践

    根据提供的标题、描述和标签,本文将围绕“Java并发编程实践”这一主题展开,深入探讨Java并发编程的基础概念、核心原理以及实际应用技巧。 ### 一、Java并发编程概述 Java并发编程是Java开发中一个非常重要的领域...

Global site tag (gtag.js) - Google Analytics