11.5 高级实用工具

在Java平台出现之后的很长一段时间内,开发多线程程序只能使用Java平台提供的synchronized和volatile关键词,以及Object类中的wait、notify和notifyAll等方法。这些关键词和方法的抽象层次比较低,在程序开发中使用起来比较繁琐,而且容易产生错误。在多线程开发中,线程之间的交互方式存在某些固定的模式,比如常见的生产者-消费者和读者-写者模式。如果可以把这些模式抽象成高层的API,那么使用起来会非常方便。J2SE 5.0引入的java.util.concurrent包为多线程程序开发提供了高层的API,可以满足日常开发中常见的需求。

11.5.1 高级同步机制

虽然非阻塞方式的性能优于阻塞式方式,但是并非所有场景都采用非阻塞式的实现方式,在很多情况下仍然需要使用基于锁机制的阻塞式实现方式。Java中基本的阻塞式实现方式是基于synchronized关键词和Object类的wait、notify和notifyAll方法。通过synchronized关键词可以获取监视器对象上的锁,不过这种锁的获取和释放都是隐式进行的,由线程在执行synchronized方法或代码块时自动完成。使用synchronized关键词的问题在于加锁的范围是固定的,无法把锁在对象之间进行传递,使用起来并不灵活。不过它的好处是使用起来简单,不容易出现错误。如果需要使用更加灵活的锁机制,可以使用java.util.concurrent.locks包中提供的API。

java. util.concurrent.locks包中的重要接口之一是Lock。Lock接口表示的是一个锁,可以通过其中的lock方法来获取锁,而unlock方法用来释放锁。使用Lock接口的代码需要保证锁总是被释放。一般把unlock方法的调用放在finally代码块中。Lock接口提供了几种不同的获取锁的方式。使用lock方法获取锁的方式类似于synchronized关键词。如果调用lock方法时无法获取锁,那么当前线程会处于等待状态,直到成功获取锁。与lock方法相似的lockInterruptibly方法允许当前线程在等待获取锁的过程中被中断。所以调用lockInterruptibly方法时要处理InterruptedException异常。除了通过阻塞式的获取锁外,Lock接口也提供了tryLock方法以非阻塞的方式获取锁。如果在调用tryLock方法时无法获取锁,那么tryLock方法只返回false,不会阻塞当前线程。利用tryLock方法的另外一种重载形式可以指定超时时间。如果指定了超时时间,当无法获取锁时,当前线程会处于阻塞状态,但是等待的时间不会超过指定的超时时间,同时线程也是可以被中断的。

另外一个与锁相关的接口是ReadWriteLock。ReadWriteLock接口实际上表示的是两个锁,一个是读取操作使用的共享锁,另外一个是写入操作使用的排他锁。可以通过ReadWriteLock接口的readLock和writeLock方法来获取表示对应的锁的Lock接口的实现对象。ReadWriteLock接口适合于解决与常见的读者-写者问题类似的场景。在没有线程进行写入操作时,进行读取操作的多个线程都可以获取读取锁;而进行写入操作的线程只有在获取写入锁之后才能进行写入操作。多个线程可以同时进行读取操作,但是同一时刻只允许一个线程进行写入操作。ReadWriteLock接口可以在很多情况下提高多线程程序的性能。在大多数情况下,对一个数据结构的读取操作的次数要远多于写入操作的次数。ReadWriteLock接口允许多个线程同时进行读取操作,这样可以提高使用该数据结构时的吞吐量。如果对数据结构的访问模式不满足这种特性,比如有较多的写入操作,则使用ReadWriteLock接口会降低性能。

在java.util.concurrent.locks包中提供了Lock接口和ReadWriteLock接口的基本实现,分别是ReentrantLock类和ReentrantReadWriteLock类。这两个实现类的共同特征是可重入性,即允许一个线程多次获取同一个锁。ReentrantLock类的对象可以有一个所有者线程,表示上一次成功获取该锁,但还没有释放锁的线程。ReentrantLock类的对象同时保存了所有者线程在该对象上的加锁次数。通过getHoldCount方法可以获取当前的加锁次数。如果ReentrantLock类的对象当前没有所有者线程,则当前线程获取锁的操作会成功,加锁次数为1。在随后的操作中,当前线程可以再次获取该锁,这也是可重入的含义所在。每次加锁操作会使加锁次数增加1,而每一次调用unlock方法释放锁会使加锁次数减1。当加锁次数变为0时,该锁会被释放,可以被其他线程获取。代码清单11-12给出了使用ReentrantLock类实现的标识符生成器的代码示例。代码中的getNext方法也给出了Lock接口的基本使用方式。开始时使用lock方法来加锁,接着把所要执行的操作放在try-finally代码块中,在finally代码块中通过unlock方法来释放锁。

代码清单11-12 使用ReentrantLock类实现的生成唯一标识符的Java类


public class LockIdGenerator{

private final ReentrantLock lock=new ReentrantLock();

private int value=0;

public int getNext(){

lock.lock();

try{

return value++;

}finally{

lock.unlock();

}

}

}


在创建ReentrantLock类的对象时,可以通过一个额外的boolean类型的参数来声明使用更加公平的加锁机制。在使用锁机制时会遇到的一个问题是线程的饥饿问题。当多个线程同时竞争某个锁时,可能有的线程一直无法成功获取锁,一直处于无法运行的状态。线程饥饿是有些程序应该避免的问题。如果在创建ReentrantLock类的对象时添加了额外的参数true,则ReentrantLock类的对象会使用相对公平的锁分配策略。当锁处于可被获取状态时,在由于尝试获取该锁而处于等待状态的线程中,等待时间最长的线程会成功获取这个锁。这就避免了线程的饥饿问题。不过需要注意的是不带参数的tryLock方法会忽略公平模式的设置。ReentrantReadWriteLock类是ReadWriteLock接口的可重入的实现类。

可重入锁的优势在于减少了锁在各个线程之间的传递次数,可以提高程序的吞吐量。这里以线程安全的散列表的实现为例来进行说明。在散列表实现中,每个对内部状态进行修改的公开方法都由锁来保护。在方法执行之前都要求线程获取锁,在方法调用完成之后,锁被释放。在一段程序的执行过程中,通常会多次使用同一个散列表对象上的方法。如果在散列表对象中使用的是可重入的锁,则当前线程只有在调用第一个方法时需要与其他线程竞争锁。在成功加锁之后,后续调用的方法可以通过重入的方式来快速获取锁,这就降低了加锁的代价,锁的所有者也没有发生改变。如果锁的实现不是可重入的,那么线程在每次调用方法时都需要与其他线程竞争锁的所有权。如果没能获取锁,则当前线程需要等待,直到再次获取锁。在这种情况下,整段代码执行中的很大一部分时间都花费在加锁与解锁操作上。为了提高程序整体的吞吐量,应该尽可能使用可重入的锁。

Lock接口替代synchronized关键词,相对应的Condition接口替代Object类的wait、notify和notifyAll方法。就如同使用wait、notify和notifyAll方法时不能脱离synchronized关键词一样,使用Condition接口时也需要与一个对应的Lock接口的实现对象关联起来。通过Lock接口的newCondition方法可以创建新的Condition接口的实现对象。在调用Condition接口的方法之前,也需要使用Lock接口的方法来获取锁。

Condition接口提供了多个类似Object类的wait方法的方法,最基本的是await方法,调用该方法会使当前线程进入等待状态,直到被唤醒或被中断。另外一种await方法的重载形式可以指定超时时间。方法awaitNanos以纳秒数为单位指定超时时间,该方法的返回值是剩余等待时间的估计值。类似的awaitUntil方法也可以指定超时时间,只不过指定的不是要经过的时间,而是超时发生的时间点,参数是一个java.util.Date类的对象。前面几种等待方法都会响应其他线程发出的中断请求,而awaitUninterruptibly方法则不会处理中断请求。如果线程通过调用awaitUninterruptibly方法进入等待状态,那么,当收到中断请求时,线程仍然会继续处于等待状态,直到被唤醒。当线程从awaitUninterruptibly方法返回时,其内部的中断标记会被设置,以表明曾经有中断请求发生。与Object类的wait方法相同,当线程由于调用await等方法进入等待状态时,会释放其持有的锁。

与Condition接口中的等待方法相对应的是signal和signalAll方法,相当于Object类中的notify和notifyAll方法。这两个方法的含义与notify和notifyAll方法是相同的。代码清单11-13给出了Lock接口和Condition接口的一般使用方式,类似于代码清单11-7中对Object类的wait方法的使用方式。

代码清单11-13 Lock接口和Condition接口的一般使用方式


Lock lock=new ReentrantLock();

Condition condition=lock.newCondition();

lock.lock();

try{

while(/逻辑条件不满足/){

condition.await();

}

}finally{

lock.unlock();

}