自从Java 1.5 发行版本开始,Java平台就提供了更高级的并发工具,他们可以完成以前必须在wait和notify上手写代码来完成的各项工作。既然正确的使用wait和notify比较困难,就应该用更高级的并发工具来代替。
java.util.concurrent中更高级的工具分成三类:Executor Framework、并发集合(Concurrent Collection)以及同步器(Synchronizer)。
并发集合为标准的集合接口(如List、Queue和Map)提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢。
这意味着客户无法原子的对并发集合进行方法调用。因此有些集合接口已经通过依赖状态的修改操作进行了扩展,它将几个基于操作合并到了单个原子操作中。例如,ConcurrentMap扩展了Map接口,并添加了几个方法,包括putIfAbsent(key, value),当键没有映射时会替她插入一个映射,并返回与键关联的前一个值,如果没有这样的值,则返回null。
ConcurrentHashMap除了提供卓越的并发性之外,速度也非常快。除非不得已,否则应该优先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或者Hashtable。只要用并发Map替换老式的同步Map,就可以极大地提升并发应用程序的性能。更一般的,应该优先使用并发集合,而不是使用外部同步的集合。
有些集合接口已经通过阻塞操作进行了扩展,他们会一直等待(或者阻塞)到可以成功执行为止。例如,BlockingQueue扩展了Queue接口,并添加了包括take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空,就等待。这样就允许将阻塞队列用于工作队列,也称作生产者-消费者队列,一个或者多个生产者线程在工作队列中添加工作项目,并且当工作项目可用时,一个或者多个消费者线程,则从工作队列中取出队列并处理工作项目。不出所料,大多数ExecutorService实现(包括ThreadPoolExecutor)都使用BlockingQueue。
同步器是一些使线程能够等待另一个线程的对象,允许他们协调动作。最常用的同步器是CountDownLatch和Semaphore。较不常用的是CyclicBarrier和Exchanger。
倒计数锁存器(CountDownLatch)是一次性的障碍,允许一个或者多个线程等待一个或者多个其他线程来做某些事情。CountDownLatch的唯一构造器带有一个int类型的参数,这个int参数是指允许所有在等待的线程被处理之前,必须在锁存器上调用countDown方法的次数。
要在这个简单的基本类型之上构建一些有用的东西,做起来是相当的容易。例如,假设想要构建一个简单地框架,用来给一个工作的并发执行定时。这个框架中包含单个方法,这个方法带有一个执行该动作的executor,一个并发级别(表示要并发执行该动作的次数),以及表示该动作的runnable。所有的工作线程(worker thread)自身都准备好,要在timer线程启动时钟之前运行该动作(为了实现准确地定时,这是必须的)。当最后一个工作线程准备好运行该动作时,timer线程就“发起头炮”,同时允许工作线程执行该动作。一旦最后一个工作线程执行完成该动作,timer线程就立即停止计时。
还有一些细节值得注意。传递给timer方法的executor必须允许创建至少与指定并发级别一样多的线程,否则就永远不会结束。这就是线程饥饿死锁。如果工作线程捕捉到InterruptedException,就会利用习惯用法Thread.currentThread().interrupt()重新断言中断,并从他的run方法中返回。这样就允许executor在必要的时候处理中断,事实上也理当如此。最后,对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills。System.nanoTIme更加准确也更加精确,它不受系统时钟的调整所影响。
虽然你时钟应该优先使用并发工具,而不是使用wait和notify,但可能必须维护使用了wait和notify的遗留代码。wait方法被用来使线程等待某个条件,它必须在同步区域内部被调用,这个同步区域将对象锁定在了调用wait方法的对象上。
始终应该使用wait循环模式来调用wait方法;永远不要在循环之外调用wait方法。循环会在等待之前和之后测试条件。
在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性是必要的。如果条件已经成立,并且在线程等待之前,notify(或者notifyAll)方法已经被调用,则无法保证该线程将会从等待中苏醒过来。
在等待之后测试条件,如果 条件不成立的话继续等待,这对于确保安全性是必要的。当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系。当条件不成立时,有下面一些理由可使一个线程苏醒过来:
一个相关的话题是,为了唤醒正在等待的线程,你应该使用notfiy还是notifyAll。一种常见的说法是,你总是应该使用notifyAll。这是合理而保守的建议。它总会产生正确的结果,因为它可以保证你将会唤醒所有需要被唤醒的线程你可能也会唤醒其他一些线程,但是这不会影响程序的正确性,这些线程醒来之后,会检查他们正在等待的条件如果发现条件并不满足,就会继续等待。
从优化的角度来看,如果处于等待状态的所有线程都在等待同一个条件,而每次只有一个线程可以从这个条件中被唤醒,那么你应该选择调用notify,而不是notifyAll。
即使这些条件都是真的,也许还是有理由使用notifyAll而不是notify。就好像把wait调用放在一个循环中,以避免在公有可访问对象上的意外或恶意的通知一样,与此类似,使用notifyAll代替notify可以避免来自不想关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使真正的接收线程无限的等待下去。
简而言之,直接使用wait和notify就像用“并发汇编语言”进行编程一样,而java.util.concurrent则提供了更高级的语言。没有理由在新代码中使用wait和notify,即使有、也是极少的。如果你在维护使用wait和notify的代码,务必确保始终是利用标准的模式从while循环内部调用wait。一般情况下,你应该优先使用notifyAll,而不是使用notify。如果使用notify,请一定要小心,以确保程序的活性。