目录
同步容器
Vector,Hashtable,Collections.synchronizedXXX方法产生的类,都是同步容器类,他们是线程安全的,但是关于他们的复合操作,需要使用客户端加锁进行保护。例如下复合操作:
/**
* UnsafeVectorHelpers
* <p/>
* Compound actions on a Vector that may produce confusing results
*
* @author Brian Goetz and Tim Peierls
*/
public class UnsafeVectorHelpers {
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);
}
}
这2个复合操作在多线程环境下会抛出ArrayIndexOutOfBoundsException的异常。所以使用客户端加锁进行同步:
/**
* SafeVectorHelpers
* <p/>
* Compound actions on Vector using client-side locking
*
* @author Brian Goetz and Tim Peierls
*/
public class SafeVectorHelpers {
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);
}
}
}
出于同样的问题我们对下面的迭代代码加上锁:
synchronized (vector) {
for(int i=0;i<vetor.size();i++)
dosonething(vector.get(i));
}
出现的问题:加上锁,防止其他线程在迭代期间修改vector,这样完全阻止了其他线程在此期间访问vector,消弱了并发。
如果容器很大,dosonething耗时很长,那么其他线程访问容器时需要等待很长一段时间,这就破坏了程序的可伸缩性。而且保持锁时间越长,等待锁的线程就会越多,对锁的竞争就会越激烈,线程的吞吐率,cpu的效能都会受到影响。
复制容器然后迭代是对客户端加锁的一个替代方法。但是复制容器会有明显的性能开销。
并发容器
为多线程的并发而设计。ConcurrentHashMap用来替代SynchronizedMap;当多数操作为读取时,CopyOnWriteArrayList是List的并发实现;ConcurrentMap接口加入了常见的复合操作:V putIfAbsent(K key, V value);替换、条件删除。
Queue和BlockingQueue,BlockingQueue中有阻塞的方法,也有非阻塞立即返回的方法。
ConcurrentHashMap使用了分离锁(?),这个技术使得任意个读线程,有限数量的写线程,读写线程这三种情况下都可以并发访问或修改Map,带来了更高的吞吐率,同时几乎也没损失单个线程的访问性能。
且,ConcurrentHashMap返回的迭代器具有弱一致性(?),可以容许并发修改,当迭代器创建时,它会遍历已有的元素,并可以(但是不保证)在迭代器被构造后将修改操作反映给容器。
ConcurrentHashMap 不能被加锁来执行独占访问,因此我们无法使用客户端加锁来创建新的原子操作。一些常见的复合操作,例如“若没有则添加”、“若相等则移除(Remove-If-Equal)”、“若相等则替换(Replace-If-Equla)”等,都已经实现为原子操作并且在 ConcurrentMap 的接口中声明。
CopyOnWriteArrayList的实现可以参见源码,使用volatile修饰底层数组,使得每次的加锁修改都能被其他线程立即可见,这样可以使其他的线程访问读取时不用加锁读取。而且每次的修改都会先复制一份原来的数组然后到新的数组中,然后把修改的元素写入到新数组中,最后把新数组赋值给底层数组变量。
一个生产者和消费者的例子:
/**
* ProducerConsumer
* <p/>
* Producer and consumer tasks in a desktop search application
*
* @author Brian Goetz and Tim Peierls
*/
public class ProducerConsumer {
static class FileCrawler implements Runnable {
private final BlockingQueue<File> fileQueue;
private final FileFilter fileFilter;
private final File root;
public FileCrawler(BlockingQueue<File> fileQueue,
final FileFilter fileFilter,
File root) {
this.fileQueue = fileQueue;
this.root = root;
this.fileFilter = new FileFilter() {
public boolean accept(File f) {
return f.isDirectory() || fileFilter.accept(f);
}
};
}
private boolean alreadyIndexed(File f) {
return false;
}
public void run() {
try {
crawl(root);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void crawl(File root) throws InterruptedException {
File[] entries = root.listFiles(fileFilter);
if (entries != null) {
for (File entry : entries)
if (entry.isDirectory())
crawl(entry);
else if (!alreadyIndexed(entry))
fileQueue.put(entry);
}
}
}
static class Indexer implements Runnable {
private final BlockingQueue<File> queue;
public Indexer(BlockingQueue<File> queue) {
this.queue = queue;
}
public void run() {
try {
while (true)
indexFile(queue.take());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void indexFile(File file) {
// Index the file...
};
}
private static final int BOUND = 10;
private static final int N_CONSUMERS = Runtime.getRuntime().availableProcessors();
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();
}
Deque和 BlockingDuque,它们分别对 Queue 和 BlockingQueue 进行了扩展。Deque 是一个双端队列,实现了在队列头和队列尾的高效插入和移除。
Work-stealing模式,每一个消费者线程都有自己的双端队列,如果一个消费者线程处理完了自己双端队列中的工作任务,它还可以窃取其他消费者的双端队列中的末尾的工作任务。因为各个消费者线程不会竞争一个共享的任务队列,所以消费者线程大多数时候访问自己的额双端队列。
阻塞和中断
线程可能阻塞或暂停执行,原因有多种:等待 I/O 操作结束,等待获得一个锁,等待从 Thread.sleep 方法中醒来,或是等待另一个线程的计算结果。当线程阻塞时,它通常被挂起,并被设置成线程阻塞的某个状态(BLOCKED,WAITING,TIME_WAITING)。被阻塞的线程必须等待一个事件的发生才能继续进行,这个事件是Thread它自己无法控制的,通常花费很长时间,例如等待IO操作完成、锁可用了、外部计算结束。当这些外部事件发生时,Thread被值回RUNNABLE状态,重新获得调度的机会。
一个线程不能迫使其他线程停止正在做的事情,或者去做其他事情。然而在应用级别的停止任务就是取消一个任务,从时间角度来看,响应中断的阻塞方法可以更容易地取消一个任务。
当在代码中调用了一个将抛出 InterruptedException 异常的方法时,你自己的方法也就变成了一个阻塞方法,并且需要处理对中断的响应。对此,有两种基本选择:
传递 InterruptedException:避开这个异常通常是最明确的策略----只需要把 InterruptedException 传递给方法的调用者。传递 InterruptedException 的方法包括,根本不捕获该异常,或者捕获该异常,然后在执行某种简单的清理工作后再次抛出这个异常。
恢复中断。有时候不能抛出 InterruptedException。例如当代码是 Runable 的一部分时。在这些情况下,必须捕获 InterruptedException,并通过调用当前线程上的 interupt 方法恢复中断状态,这样在调用栈中更高层的代码将看到引发一个中断。如下代码恢复中断状态以避免屏蔽中断。
/**
* TaskRunnable
* <p/>
* Restoring the interrupted status so as not to swallow the interrupt
*
* @author Brian Goetz and Tim Peierls
*/
public class TaskRunnable implements Runnable {
BlockingQueue<Task> queue;
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
// restore interrupted status
Thread.currentThread().interrupt();
}
}
void processTask(Task task) {
// Handle the task
}
interface Task {
}
}
同步工具类
除过阻塞队列外,同步工具类还包裹信号量 Semaphore、栅栏 Barrier、闭锁 Latch,他们有类似的结构特性:他们封装状态,而这些状态决定着线程执行到某一点时是通过还是等待;他们提供操控状态的方法;以及提供高效地等待进入到期望状态的方法。
闭锁Latch可以用来确保特定活动直到其他活动完成后才发生。如下:
/**
* TestHarness
* <p/>
* Using CountDownLatch for starting and stopping threads in timing tests
*
* @author Brian Goetz and Tim Peierls
*/
public class TestHarness {
public long timeTasks(int nThreads, final Runnable task)
throws InterruptedException {
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
Thread t = new Thread() {
public void run() {
try {
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException ignored) {
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end - start;
}
}
闭锁构造时也会打造一些钥匙,他们用计数器来表示,闭锁会使用await方法锁住程序的一处,让所有经过此处的线程在此等待,我们可以使用闭锁的钥匙去打开闭锁,开锁的方法是countDown,每次开锁都会使用一些钥匙,当闭锁构造时打造的钥匙都用尽时,这把闭锁才会打开,也就是程序的那一处可以让所有的线程通过了。
FutureTask表示的计算时通过 Callable 来实现的,相当于一种可生成结果的 Runnable,并且可以处于以下四种状态:
/** State value representing that task is ready to run */
private static final int READY = 0;
/** State value representing that task is running */
private static final int RUNNING = 1;
/** State value representing that task ran */
private static final int RAN = 2;
/** State value representing that task was cancelled */
private static final int CANCELLED = 4;
FutureTask.get 的行为取决于任务的状态。如果任务已经完成,那么 get 会立即返回结果,否则 get 将阻塞直到任务进入完成状态,然后返回结果或抛出异常。如下例:
/**
* Preloader
*
* Using FutureTask to preload data that is needed later
*
* @author Brian Goetz and Tim Peierls
*/
public class Preloader {
ProductInfo loadProductInfo() throws DataLoadException {
return null;
}
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) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException)
throw (DataLoadException) cause;
else
throw LaunderThrowable.launderThrowable(cause);
}
}
interface ProductInfo {
}
}
class DataLoadException extends Exception { }
/**
* StaticUtilities
*
* @author Brian Goetz and Tim Peierls
*/
public class LaunderThrowable {
/**
* Coerce an unchecked Throwable to a RuntimeException
* <p/>
* If the Throwable is an Error, throw it; if it is a
* RuntimeException return it, otherwise throw IllegalStateException
*/
public static RuntimeException launderThrowable(Throwable t) {
if (t instanceof RuntimeException)
return (RuntimeException) t;
else if (t instanceof Error)
throw (Error) t;
else
throw new IllegalStateException("Not unchecked", t);
}
}
计数信号量(Counting Semaphore)用来控制同时访问某个特定资源的线程数量,或者同时执行某个指定操作的线程数量。计数信号量可以用来实现某种资源池,或者对容器施加边界。
一个Semaphore 管理一组有效的许可证(Permit)集,许可证的初始数量可以通过构造函数来指定。程序通过acquire方法获得许可证,若没有可用的许可证它会被阻塞,直到有可用的(或直到被中断,或直到操作超时)就解除阻塞。而release方法会归还许可证。
/**
* BoundedHashSet
* <p/>
* Using Semaphore to bound a collection
*
* @author Brian Goetz and Tim Peierls
*/
public class BoundedHashSet <T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
} finally {
if (!wasAdded)
sem.release();
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
关卡Barrier 类似于闭锁,它能在程序的某处设置关卡以便阻塞一组线程直到所有线程到达此处,至于所有线程到底是多少个线程?这要看你构造关卡时传递的parties参数。Barrier也使用await方法阻塞程序在某一处,直到所有线程都到达程序此处就解除阻塞,如果在所有线程到达程序此处之前有await超时或者有阻塞中的线程被中断,那么关卡被标记为是一个失败的关卡,此时被中断的线程会抛出InterruptedException,已经阻塞的线程会抛出BrokenBarrierException,即将调用await的线程也会抛出BrokenBarrierException。在所有线程成功推倒关卡的时候,Barrier会在最后一个到达关卡的线程里执行构造关卡时提前定制的任务,它是一个Runnable。这里我不讲它和Latch的区别,因为他们俩在大多数时候都可以修改程序达到互换的效果。
/**
* CellularAutomata
*
* Coordinating computation in a cellular automaton with CyclicBarrier
*
* @author Brian Goetz and Tim Peierls
*/
public class CellularAutomata {
private final Board mainBoard;
private final CyclicBarrier barrier;
private final Worker[] workers;
public CellularAutomata(Board board) {
this.mainBoard = board;
int count = Runtime.getRuntime().availableProcessors();
this.barrier = new CyclicBarrier(count,
new Runnable() {
public void run() {
mainBoard.commitNewValues();
}});
this.workers = new Worker[count];
for (int i = 0; i < count; i++)
workers[i] = new Worker(mainBoard.getSubBoard(count, i));
}
private class Worker implements Runnable {
private final Board board;
public Worker(Board board) { this.board = board; }
public void run() {
while (!board.hasConverged()) {
for (int x = 0; x < board.getMaxX(); x++)
for (int y = 0; y < board.getMaxY(); y++)
board.setNewValue(x, y, computeValue(x, y));
try {
barrier.await();
} catch (InterruptedException ex) {
return;
} catch (BrokenBarrierException ex) {
return;
}
}
}
private int computeValue(int x, int y) {
// Compute the new value that goes in (x,y)
return 0;
}
}
public void start() {
for (int i = 0; i < workers.length; i++)
new Thread(workers[i]).start();
mainBoard.waitForConvergence();
}
interface Board {
int getMaxX();
int getMaxY();
int getValue(int x, int y);
int setNewValue(int x, int y, int value);
void commitNewValues();
boolean hasConverged();
void waitForConvergence();
Board getSubBoard(int numPartitions, int index);
}
}
构建高效,可伸缩的高速缓存
来看一段使用hashmap和同步构建的缓存:
/**
* Memoizer1
*
* Initial cache attempt using HashMap and synchronization
*
* @author Brian Goetz and Tim Peierls
*/
public class Memoizer1 <A, V> implements Computable<A, V> {
@GuardedBy("this") private final Map<A, V> cache = new HashMap<A, V>();
private final Computable<A, V> c;
public Memoizer1(Computable<A, V> c) {
this.c = c;
}
public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}
interface Computable <A, V> {
V compute(A arg) throws InterruptedException;
}
class ExpensiveFunction
implements Computable<String, BigInteger> {
public BigInteger compute(String arg) {
// after deep thought...
return new BigInteger(arg);
}
}
多线程调用上面的compute方法会出现如下图的问题,Computable的方法耗时很长,而Memoizer1的compute方法只能让线程一个一个通过,可以说这里只是串行地同步,并发显得不够灵活,访问效率低下,伸缩性差。
改进1,如果我们把hashmap改为ConcurrentHashMap,就可以拆分Memoizer1的compute方法这个大的同步块为2个小的同步块---2个ConcurrentHashMap的安全方法。如下:
/**
* Memoizer2
* <p/>
* Replacing HashMap with ConcurrentHashMap
*
* @author Brian Goetz and Tim Peierls
*/
public class Memoizer2 <A, V> implements Computable<A, V> {
private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
private final Computable<A, V> c;
public Memoizer2(Computable<A, V> c) {
this.c = c;
}
public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}
多线程调用上面的compute方法其实也会出现问题,如下图:
如上问题,重复调用Computable耗时计算同一个值,而缓存的目的就是要避免重复计算,可见这是缓存的一个bug,要修正。
改进2,所以我们想到了FutureTask。
public class Memoizer3 <A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizer3(Computable<A, V> c) {
this.c = c;
}
public V compute(A arg) throws InterruptedException {
Future<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 = ft;
cache.put(arg, ft);
ft.run();
}
try {
return f.get();
}catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
}
这里也可能出现上面同样的问题,多个线程可能同时计算相同的值。
改进3,使用ConcurrentHashMap的原子方法putIfAbsent:
/**
* Memoizer
* <p/>
* Final implementation of Memoizer
*
* @author Brian Goetz and Tim Peierls
*/
public class Memoizer <A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
= new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
public V compute(final A arg) throws InterruptedException {
while (true) {
Future<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.launderThrowable(e.getCause());
}
}
}
}
转载请注明出处。