并发 Collections 提供了线程安全、经过良好调优的数据结构,简化了并发编程。然而, 在一些情形下,开发人员需要更进一步,思考如何调节和/或限制线程执行。由于 java.util.concurrent 的总体目标是简化多线程编程,您可能希望该包包含同步实用程序,而 它确实包含。
本文是 第 1 部分 的延续,将介绍几个比核心语言原语(监视器)更高级的同步结构,但 它们还未包含在 Collection 类中。一旦您了解了这些锁和门的用途,使用它们将非常直观。
1. Semaphore
在一些企业系统中,开发人员经常需要限制未处理的特定资源请求(线程/操作)数量,事 实上,限制有时候能够提高系统的吞吐量,因为它们减少了对特定资源的争用。尽管完全可以 手动编写限制代码,但使用 Semaphore 类可以更轻松地完成此任务,它将帮您执行限制,如清 单 1 所示:
清单 1. 使用 Semaphore 执行限制
import java.util.*;import java.util.concurrent.*;public class SemApp{ public static void main(String[] args) { Runnable limitedCall = new Runnable() { final Random rand = new Random(); final Semaphore available = new Semaphore(3); int count = 0; public void run() { int time = rand.nextInt(15); int num = count++; try { available.acquire(); System.out.println("Executing " + "long-running action for " + time + " seconds... #" + num); Thread.sleep(time * 1000); System.out.println("Done with #" + num + "!"); available.release(); } catch (InterruptedException intEx) { intEx.printStackTrace(); } } }; for (int i=0; i<10; i++) new Thread(limitedCall).start(); }}
即使本例中的 10 个线程都在运行(您可以对运行 SemApp 的 Java 进程执行 jstack 来验 证),但只有 3 个线程是活跃的。在一个信号计数器释放之前,其他 7 个线程都处于空闲状 态。(实际上,Semaphore 类支持一次获取和释放多个 permit,但这不适用于本场景。)
2. CountDownLatch
如果 Semaphore 是允许一次进入一个(这可能会勾起一些流行夜总会的保安的记忆)线程 的并发性类,那么 CountDownLatch 就像是赛马场的起跑门栅。此类持有所有空闲线程,直到 满足特定条件,这时它将会一次释放所有这些线程。
清单 2. CountDownLatch:让我们去赛马吧!
import java.util.*;import java.util.concurrent.*;class Race{ private Random rand = new Random(); private int distance = rand.nextInt(250); private CountDownLatch start; private CountDownLatch finish; private List horses = new ArrayList (); public Race(String... names) { this.horses.addAll(Arrays.asList(names)); } public void run() throws InterruptedException { System.out.println("And the horses are stepping up to the gate..."); final CountDownLatch start = new CountDownLatch(1); final CountDownLatch finish = new CountDownLatch(horses.size ()); final List places = Collections.synchronizedList(new ArrayList ()); for (final String h : horses) { new Thread(new Runnable() { public void run() { try { System.out.println(h + " stepping up to the gate..."); start.await(); int traveled = 0; while (traveled < distance) { // In a 0-2 second period of time.... Thread.sleep(rand.nextInt(3) * 1000); // ... a horse travels 0-14 lengths traveled += rand.nextInt(15); System.out.println(h + " advanced to " + traveled + "!"); } finish.countDown(); System.out.println(h + " crossed the finish!"); places.add(h); } catch (InterruptedException intEx) { System.out.println("ABORTING RACE!!!"); intEx.printStackTrace(); } } }).start(); } System.out.println("And... they're off!"); start.countDown(); finish.await(); System.out.println("And we have our winners!"); System.out.println(places.get(0) + " took the gold..."); System.out.println(places.get(1) + " got the silver..."); System.out.println("and " + places.get(2) + " took home the bronze."); }}public class CDLApp{ public static void main(String[] args) throws InterruptedException, java.io.IOException { System.out.println("Prepping..."); Race r = new Race( "Beverly Takes a Bath", "RockerHorse", "Phineas", "Ferb", "Tin Cup", "I'm Faster Than a Monkey", "Glue FacTory Reject" ); System.out.println("It's a race of " + r.getDistance() + " lengths"); System.out.println("Press Enter to run the race...."); System.in.read(); r.run(); }}
注意,在 清单 2 中,CountDownLatch 有两个用途:首先,它同时释放所有线程,模拟马 赛的起点,但随后会设置一个门闩模拟马赛的终点。这样,“主” 线程就可以输出结果。为了 让马赛有更多的输出注释,可以在赛场的 “转弯处” 和 “半程” 点,比如赛马跨过跑道的 四分之一、二分之一和四分之三线时,添加 CountDownLatch。
3. ExecuTor
清单 1 和 清单 2 中的示例都存在一个重要的缺陷,它们要求您直接创建 Thread 对象。 这可以解决一些问题,因为在一些 JVM 中,创建 Thread 是一项重量型的操作,重用现有 Thread 比创建新线程要容易得多。而在另一些 JVM 中,情况正好相反:Thread 是轻量型的, 可以在需要时很容易地新建一个线程。当然,如果 Murphy 拥有自己的解决办法(他通常都会 拥有),那么您无论使用哪种方法对于您最终将部署的平台都是不对的。
JSR-166 专家组在一定程度上预测到了这一情形。Java 开发人员无需直接创建 Thread,他 们引入了 ExecuTor 接口,这是对创建新线程的一种抽象。如清单 3 所示,ExecuTor 使您不 必亲自对 Thread 对象执行 new 就能够创建新线程:
清单 3. ExecuTor
ExecuTor exec = getAnExecuTorFromSomeplace();exec.execute(new Runnable() { ... });
使用 ExecuTor 的主要缺陷与我们在所有工厂中遇到的一样:工厂必须来自某个位置。不幸 的是,与 CLR 不同,JVM 没有附带一个标准的 VM 级线程池。
ExecuTor 类实际上 充当着一个提供 ExecuTor 实现实例的共同位置,但它只有 new 方法 (例如用于创建新线程池);它没有预先创建实例。所以您可以自行决定是否希望在代码中创 建和使用 ExecuTor 实例。(或者在某些情况下,您将能够使用所选的容器/平台提供的实例。 )
ExecuTorService 随时可以使用
尽管不必担心 Thread 来自何处,但 ExecuTor 接口缺乏 Java 开发人员可能期望的某种功 能,比如结束一个用于生成结果的线程并以非阻塞方式等待结果可用。(这是桌面应用程序的 一个常见需求,用户将执行需要访问数据库的 UI 操作,然后如果该操作花费了很长时间,可 能希望在它完成之前取消它。)
对于此问题,JSR-166 专家创建了一个更加有用的抽象(ExecuTorService 接口),它将线 程启动工厂建模为一个可集中控制的服务。例如,无需每执行一项任务就调用一次 execute() ,ExecuTorService 可以接受一组任务并返回一个表示每项任务的未来结果的未来列表。
4. ScheduledExecuTorServices
尽管 ExecuTorService 接口非常有用,但某些任务仍需要以计划方式执行,比如以确定的 时间间隔或在特定时间执行给定的任务。这就是 ScheduledExecuTorService 的应用范围,它 扩展了 ExecuTorService。
如果您的目标是创建一个每隔 5 秒跳一次的 “心跳” 命令,使用 ScheduledExecuTorService 可以轻松实现,如清单 4 所示:
清单 4. ScheduledExecuTorService 模拟心跳
import java.util.concurrent.*;public class Ping{ public static void main(String[] args) { ScheduledExecuTorService ses = ExecuTors.newScheduledThreadPool(1); Runnable pinger = new Runnable() { public void run() { System.out.println("PING!"); } }; ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS); }}
这项功能怎么样?不用过于担心线程,不用过于担心用户希望取消心跳时会发生什么,也不 用明确地将线程标记为前台或后台;只需将所有的计划细节留给 ScheduledExecuTorService。
顺便说一下,如果用户希望取消心跳,scheduleAtFixedRate 调用将返回一个 ScheduledFuture 实例,它不仅封装了结果(如果有),还拥有一个 cancel 方法来关闭计划 的操作。
5. Timeout 方法
为阻塞操作设置一个具体的超时值(以避免死锁)的能力是 java.util.concurrent 库相比 起早期并发特性的一大进步,比如监控锁定。
这些方法几乎总是包含一个 int/TimeUnit 对,指示这些方法应该等待多长时间才释放控制 权并将其返回给程序。它需要开发人员执行更多工作 — 如果没有获取锁,您将如何重新获取 ? — 但结果几乎总是正确的:更少的死锁和更加适合生产的代码。
结束语
java.util.concurrent 包还包含了其他许多好用的实用程序,它们很好地扩展到了 Collections 之外,尤其是在 .locks 和 .atomic 包中。深入研究,您还将发现一些有用的控 制结构,比如 CyclicBarrier 等。
与 Java 平台的许多其他方面一样,您无需费劲地查找可能非常有用的基础架构代码。在编 写多线程代码时,请记住本文讨论的实用程序和 上一篇文章 中讨论的实用程序。
下一次,我们将进入一个新的主题:关于 Jars 您不知道的 5 件事。
原文地址:http://www.ibm.com/developerworks/cn/java/j-5things5.html
强者能同命运的风暴抗争。