这是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,仍然讲述了关于Java并发集合API的一些应用窍门,值得大家学习。(2010.06.17最后更新)
摘要:除了便于编写并发应用的集合API外,java.util.concurrent还引入了其它的预置程序组件,这些组件能辅助你在多线程应用中控制和执行线程。Ted Neward再介绍了五个来自于java.util.concurrent的Java编程必备窍门。
通过提供线程安全,性能良好的数据结构,并发集合框架使并发编程变得更容易。然而在有些情况下,开发者需要多走一步,并要考虑控制和/或调节线程的执行。提供java.util.concurrent包的全部原因就是为了简化多线程编程--事实上正是如此。
接着第一部分,本文介绍了多个同步数据结构,这些数据结构比核心语言基本结构(监视器)的层次要高,但不会高到将它们圈囿在一个集合类中。一旦知道了这些锁与栓的用途,就能径直去用了。
1. 信号量
在有些企业级系统中,常需要开发者去控制针对特定资源的请求(线程或动作)的数量。虽然完全可能试着手工编写这样的调节程序,但使用Semaphore类会更容易些,该类会为你处理对线程的控制,如清单1所示:
清单1. 使用信号量调节线程
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类支持一次获取和释放一个以上的被许可线程,但在此处的场景中这么做没有意义。)
2. CountDownLatch
如果并发类Semaphore是被设计为在同一时刻允许"其中"一个线程执行的话,那么CountDownLatch就是赛马比赛中的起跑门。该类持有所有的线程,当遇到某个特定条件,那时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<String> horses = new ArrayList<String>(); 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<String> places = Collections.synchronizedList(new ArrayList<String>()); 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模拟了比赛的结束。一场比赛会有更多的评论,你可以在比赛的"转弯"和"半程"点添加CountDownLatch,当马匹跑过1/4程,半程和3/4程时。
3. Executor
清单1和清单2中的例子都遭遇了一个令人非常沮丧的错误,你*要直接地创建Thread对象。这是一个造成麻烦的方式,因为在有些JVM中,创建Thread对象是一件重量级的工作,所以重用而非创建新的线程要好得多。然而在另一些JVM中,情况就恰恰相反:Thread是非常轻量级的,若你需要一个线程,直接创建它则会好得多。当然,如果Murphy有他自己的方法(他经常就是这么做的),无论你使用哪种方法,对于你最终所依赖的某种Java平台都会是错误的。
JSR-166专家组在一定程度上预见到了这种情况。与让Java开发者直接创建Thread实例不同,他们推荐Executor接口,这是一个创建新线程的抽象。如果清单3所示,Executor允许你自己不必使用new操作符去创建Thread对象:
清单3. Executor
Executor exec = getAnExecutorFromSomeplace(); exec.execute(new Runnable() { });
使用Excutor的主要缺点与我们使用所有对象工厂所遇到的缺点一样:工厂必须来源于某处。不幸地是,不同于CLR,JVM并不带有一个标准的VM范围内的线程池。
Executor类只是作为获取Executor实现实例的常用地方,但它只有new方法(例如,为了创建新的线程池);它没有预创建的实例。所以,如果你想创建并使用一个能贯穿于整个程序的Executor实现,你就可以创建一个你自己的Executor实例。(或者,在有些情况下,你可以使用你所选容器/平台所提供的Executor实例。)
ExecutorService,为你服务
ExecutorService的用处在于使你不必关心Thread来自于何处,Executor接口缺乏Java开发者可能期望的一些功能,比如启动一个线程,该线程用于产生结果,它会以非阻塞方式一直等待,直到结果出现为止。(在桌面应用中这是很普通的需求,在这种应用中用户会执行一个需要访问数据库的UI操作,如果它耗时太长的话,就可能想要在它完成之前就取消这一操作。)
为此,JSR-166的专家们创造一个更为有用的抽象,ExecutorService接口,该接口将启动线程的工厂模型化为一个服务,这样就能对该服务进行集合化控制了。例如,不对每个任务调用一次execute()方法,ExecutorService能创建一个任务的集合,并可返回代表这些任务未来结果的Future集合。
4. ScheduledExecutorServices
与ExecutorService接口同样优秀,特定的任务需要以计划的形式进行执行,例如在特定的时间间隔或在特定的时刻执行给定的任务。这就是继承自ExecutorService的ScheduledExecutorService的职责范畴。
如果你的目的是创建一个"心跳"命令,该命令每5秒钟就去"ping"一次。ScheduledExecutorService会帮你做到这一点,正如你在清单4中所见的那般简单:
清单4. ScheduledExecutorService按计划去"Ping"
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. 超时方法
拥有为阻塞操作置一个确定的超时控制的能力(这样就可以避免死锁)是java.util.concurrent类库相比于旧有并发API,如针对锁的监视器,的最大优点之一。
这些方法几乎总是按int/TimeUnit对的方式进行重载,该int/TimeUnit对用于指示方法在跳出执行并将控制返回给平台之前需要等待多长时间。这要求开发者对此做更多的工作--如果没有获得锁,将如何进行恢复?--但结果却几乎总是正确的:更少的死锁,以及更加生产安全的代码。(更多关于生产就绪的代码,请见Michael Nygard的Release It!)
结论
java.util.concurrent包含有许多更优雅的工具,它们出于集合框架,但更胜之,特别是.locks和.atomic包中的类。深入挖掘之,你将发现像CyclicBarrier这样的十分有用的控制结构,甚至于更多。
下一次,我们将步入一个新的主题:你所不知道的五件关于Jar的事情。