委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可。
2.并发容器
Java 5.0提供了并发容器类,来支持多线程的并发访问。增加了ConcurrentHashMap用来替代同步且基于散列的Map,以及CopyOnWriteArrayList用于遍历操作为主要操作的情况下代替同步的List。
以上程序是扫描文件并为其建立索引的生产者-消费者例子。
下面介绍其他类型的同步工具类包括闭锁(Latch),信号量(Semaphore)、栅栏(Barrier)等
栅栏(Barrier)类似于闭锁,他能阻塞一组线程直到某个事件发生。栅栏和闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。
1.同步容器类
JDK中的同步容器类包括Vector和Hashtable等类,这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。当多个线程同时访问修改容器时,可能会产生一些意料之外的行为,如
public static Object getLast(Vector list){
int lastIndex = list.size()-1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list){
int lastIndex = list.size()-1;
list.remove(lastIndex);
}
如果A线程和B线程交替执行getLast和deleteLast方法时,就可能产生上图的错误。
一种解决方法是在客户端对容器进行加锁,使得这些新的操作和容器的其他操作一样都是原子操作,如下
public static Object getLast(Vector list){
synchronized (list) {
int lastIndex = list.size()-1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list){
synchronized (list) {
int lastIndex = list.size()-1;
list.remove(lastIndex);
}
}
在设计同步容器类的迭代器时,并没有考虑到并发修改的问题,而是采取“及时失败”的策略。实现的方式是:将计数器的变化与容器关联起来,如果在迭代期间计数器被修改,那么hasNext或next方法将抛出ConcurrentModificationException。在容器上加锁将会降低程序的可伸缩性,一种替代方法就是“克隆”容器,并在副本上进行迭代。
2.并发容器
Java 5.0提供了并发容器类,来支持多线程的并发访问。增加了ConcurrentHashMap用来替代同步且基于散列的Map,以及CopyOnWriteArrayList用于遍历操作为主要操作的情况下代替同步的List。
Java 5.0还增加了Queue和BlockingQueue。Queue在操作上不会阻塞,去掉了list的随机访问需求,从而实现更高效的并发。BlockingQueue扩展了Queue,增加了可阻塞的插入和获取等操作。在“生产者-消费者”这种设计模式中,阻塞队列非常有用。
ConcurrentHashMap
ConcurrentHashMap并不是每个方法都在同一个锁上同步并使得每次只能有一个线程访问容器,而是使用一种粒度更细的加锁机制来实现更大程度的共享,这种机制称为
分段锁,从而保证了在并发访问环境下将实现更高的吞吐量。
CopyOnWriteArrayList
“写入时复制”容器的线程安全性在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。仅当迭代操作远远多于修改操作时,才应该使用“写入时复制”容器,不然复制底层数组需要一定的开销。
3.阻塞队列和生产者-消费者模式
阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。阻塞队列支持生产者-消费者这种设计模式。这种模式消除了生产者类和消费者类之间的代码依赖性。BlockingQueue简化了生产者-消费者设计的实现过程,它支持任意数量的生产者和消费者。一种最常见的生产者-消费者设计模式就是线程池与工作队列的组合,在Executor任务执行框架中就体现了这种模式。
public class FileCrawler implements Runnable{
private final BlockingQueue<File> fileQueue;
private final FileFilter fileFileter;
private final File root;
public void run(){
try{
crawl(root);
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
private void crawl(File root) throws InterruptedException{
File[] entries = root.listFiles(fileFileter);
if(entries!=null){
for(File entry:entries){
if(entry.isDirectory())
crawl(entry);
else if(!alreadyIndexed(entry))
<strong>fileQueue.put(entry);</strong>
}
}
}
}
public class Indexer implements Runnable{
private final BlockingQueue<File> queue;
public Indexer(BlockingQueue<File> queue){
this.queue = queue;
}
public void run(){
try{
while(true)
<strong>indexFile(queue.take());//如果队列中没有文件,该方法将阻塞</strong>
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
}
<pre name="code" class="java">public static void startIndexing(File[] roots){
BlockingQueue<File> queue = new LinkedBlockingQueue<File>(BOUND);
FileFilter filter =new Filefilter(){
public boolean accept(File file){
return true;
}
};
for(File root:roots)
new Thread(new FileCrawler(queue,filter,root)).start();
for(int i=0;i<N_CONSUMERS;i++){
new Thread(new Indexer(queue)).start();
}
}
以上程序是扫描文件并为其建立索引的生产者-消费者例子。
对于可变对象,生产者-消费者这种设计与阻塞队列一起,促进了
串行线程封闭,从而将对象所有权从生产者交付给消费者。因为线程封闭对象只能由单个线程所有,具有对它的独占性。
Java还提供了支持在队列头和队列尾的高效插入和移除的双端队列Deque,具体的实现包括ArrayDeque和LinkedBlockingDeque,适用于
工作密取的模式。
4.阻塞方法与中断
阻塞操作和执行时间很长的普通操作的差别在于,被阻塞的线程必须等待某个不受它控制的事件发生后才能继续执行。当在代码中调用一个将抛出InterruptedException异常的方法时,你自己的方法也将变成了一个阻塞方法。处理中断有两种方式:1.传递InterruptedException给调用者2.捕获异常简单处理后,恢复中断。
public class TaskRunnable implements Runnable{
BlockingQueue<Task> queue;
public void run(){
try{
processTask(queue.take());
}catch(InterruptedException e){
//恢复被中断的状态
Thread.currentThread().interrupt();
}
}
}
5.同步工具类下面介绍其他类型的同步工具类包括闭锁(Latch),信号量(Semaphore)、栅栏(Barrier)等
闭锁
闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开允许所有的线程通过。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。
public long timeTasks(int nThreads, final Runnable task) throws InterruptedException{
final CountDownLatch startGate = new CountDownLatch(1);<strong>//当计数器为0时,闭锁打开</strong>
final CountDownLatch endGate = new CountDownLatch(nThread);
for(int i=0;i<nThreads;i++){
Thread t = new Thread(){
public void run(){
try{
startGate.await();
try{
task.run();
}finally{
<strong>endGate.countDown();//单个线程执行完成,结束闭锁的计数器减1</strong>
}
}catch(InterruptedException ignored){
}
}
};
t.start();
}
long start = System.nanoTime();
<strong>startGate.countDown();//类似一个发令枪,等线程都准备好之后,在放开运行
endGate.await();//等待所有线程执行完成,然后计算所有行程的运行总时间</strong>
long end=System.nanoTime();
return end-start;
}
FutureTask也可以用做闭锁,因为Future.get方法是阻塞方法,如果任务完成,则返回结果,反之,则将阻塞直到任务完成。FutureTask表示计算是通过Callable来实现的,相当于一种可生成结果的Runnable。
public class Preloader{
private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>(){
public ProductInfo call() throws DataLoadException{
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start(){
thread.start();
}
public ProductInfo get() throws DataLoadException, InterruptedException{
try{
return future.get();
}catch(ExecutionException e){
}
}
}
计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量,还可以用来实现某种资源池,或者对容器施加边界。Semaphore中管理着一组虚拟的许可,在执行操作时首先获得许可,并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可,release方法将返回一个许可信号量。
public class BoundHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundHashSet(int bound){
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);//使用Semaphore为容器的设置边界
}
public boolean add(T o) throws InterruptedException{
sem.acquire();//添加开始前acquire一个信号量
boolean wasAdded = false;
try{
wasAdded = set.add(o);
return wasAdded;
}finally{
if(!wasAdded)
sem.release();//如果添加失败,返回一个信号量
}
}
public boolean remove(T o){
boolean wasRemoved = set.remove(o);
if(wasRemoved)
sem.release();//删除成功,返回一个信号量
return wasRemoved;
}
}
栅栏(Barrier)类似于闭锁,他能阻塞一组线程直到某个事件发生。栅栏和闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。
6.构建高效且可伸缩的结果缓存
public class Memoizer<A, V> implements Computable<A,V>{
//使用ConcurrentMap来取代map,支持并发操作
private final ConcurrentMap<A,Future<V>> cache = new ConcurrentHashMap<A,Future<V>>();
private final Computable<A,V> c;
public Memozier(Computable<A,V> c){
this.c = c;
}
public V compute(final A arg) throws InterruptedException{
while(true){
Futurn<V> f = cache.get(arg);
if(f == null){
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException{
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);//保证添加键值对的时候操作的原子性
if(f == null){
f = ft;
ft.run();
}
}
try{
return f.get();//立即返回结果,否则一直阻塞直到结果计算出来在将其返回。
}catch(CancellationException e){
cache.remove(arg,f);
}catch(ExecutionException e){
throw launderThrowable(e.getCause());
}
}
}
}