5 基础构建模块
5.1 同步容器类
例如Vector和HashTable
5.1.1 同步容器类的问题
5.1.2 迭代器与ConcurrentModificationException
5.1.3 隐藏迭代器
如果状态与保护它的同步代码之间相隔越远,那么开发人员就越容易忘记在访问状态时使用正确的同步。
正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略。
5.2 并发容器
并发容器代替同步容器,可以极大地提高伸缩性并降低风险。
5.2.1 ConcurrentHashMap
5.2.2 额外的原子Map操作
public interface ConcurrentMap<K, V> extends Map<K, V> {
// 仅当K没有相应的映射值时才插入
V putIfAbsent(K key, V value);
// 仅当K被映射到V时才移除
boolean remove(K key, V value);
// 仅当K被映射到oldValue时才替换为newValue
boolean replace(K key, V oldValue, V newValue);
// 仅当K被映射到某个值时才替换为newValue
V replace(K key, V newValue);
}
5.2.3 CopyOnWriteArrayList
仅当迭代操作远远多于修改操作时,才应该使用CopyOnWrite的容器。这个准则很契合事件通知系统,在分发通知时需要迭代已注册的监听器列表,并调用每一个监听器,在大多数情况下,注册和注销事件监听器的操作远少于接收事件通知的操作。
5.3 阻塞队列和生产者-消费者模式
阻塞队列提供了可阻塞的put和take方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以使有界的也可以是无界的,无界队列永远不会充满,因此无界队列上的put方法永远不会阻塞。
在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
一个特殊的BlockingQueue是SynchronousQueue,SynchronousQueue实际上并不是一个真正的队列,因为它不会为队列中的元素维护存储空间。SynchronousQueue维护一组线程,这些线程在等待着把元素加入或者移出队列。因为SynchronousQueue没有存储功能,因此put和take会一直阻塞,直到有另一个线程已经准备好参与到交付过程中。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列。
5.3.1 示例:桌面搜索
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
/**
* 生产者,在某个文件层次中搜索符合索引标准的文件,并将文件放入工作队列
*/
public class FileCrawler implements Runnable {
private final BlockingQueue<File> fileQueue;
private final FileFilter fileFilter;
private File root;
public FileCrawler(BlockingQueue fileQueue, FileFilter fileFilter, File root) {
this.fileQueue = fileQueue;
this.fileFilter = fileFilter;
this.root = root;
}
@Override
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);
}
}
}
}
}
import java.io.File;
import java.util.concurrent.BlockingQueue;
/**
* 消费者。从队列中取出文件并建立索引。
*/
public class Indexer implements Runnable {
private final BlockingQueue<File> queue;
public Indexer(BlockingQueue<File> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
indexFile(queue.take());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
import java.io.File;
import java.io.FileFilter;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* 启动桌面搜索
*/
public class Client {
public static void main(String[] args) {
}
public static void startIndexing(File[] roots) {
BlockingQueue<File> queue = new LinkedBlockingQueue<>(BOUND);
FileFilter fileFilter = new FileFilter() {
@Override
public boolean accept(File pathname) {
return true;
}
};
for (File root : roots) {
new Thread(new FileCrawler(queue, fileFilter, root)).start();
}
for (int i = 0; i < N_CONSUMERS; i++) {
new Thread(new Indexer(queue)).start();
}
}
}
5.3.2 串行线程封闭
5.3.3 双端队列与工作密取
双端队列适用于工作密取(Work Stealing)模式。
工作密取适用于既是消费者也是生产者的问题——当执行某个工作时导致出现更多的工作。
5.4 阻塞方法与中断方法
当在代码中调用了一个会抛出InterruptedException的方法时,这个调用程序就成为了阻塞方法,并且必须要处理对中断的响应。对于库代码来说,有两种基本选择:
传递InterruptedException。 把这个异常传递给方法的调用者通常是最明智的选择。传递InterruptedException的方法包括:根本不捕获该异常,或者捕获该异常,然后在执行某种简单的清理工作后再次抛出这个异常。
恢复中断。 有时候不能抛出InterruptedException,例如代码是Runnable的一部分。在这种情况下就必须要捕获InterruptedException,并通过调用当前线程上的interrupt方法恢复中断状态,这样在调用栈中更高层的代码将看到引发了一个中断。
恢复中断不是“从中断中恢复”的意思,而是再次将线程的中断状态设置为true。参考:
https://blog.csdn.net/u014745069/article/details/87889222
程序清单 5-4-1 恢复中断状态以避免屏蔽中断
public class TaskRunnable implements Runnable {
BlockingQueue<Task> queue;
public void run() {
try {
processTask(queue.take());
} catch (InterruptedException e) {
// 恢复被中断的状态
Thread.currentThread().interrupt();
}
}
}
切忌捕获了中断异常但是不作出任何处理。这将使调用栈上更高层的代码无法对中断采取处理措施,因为线程被中断的证据已经丢失。
5.5 同步工具类
同步工具类可以是任何一个对象,只要它根据其自身的状态来协调线程的控制流。常见的同步工具类有:阻塞队列、信号量(Semaphore)、栅栏(Barrier)和闭锁(Latch)。
所有的同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态决定执行同步工具类的线程是继续执行还是等待。 此外还提供了一些方法对状态进行操作,以及另一些方法来高效地等待同步工具类进入到预期状态。
5.5.1 闭锁
CountDownLatch是一种灵活的闭锁实现。
程序清单 5-5-1-1 在计时测试中使用CountDownLatch启动和停止线程
public class TestHarness {
public long timeTask(int nThreads, final Runnable task) throws InteruptedException {
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 e) {
}
}
};
t.start();
}
long start = System.nanoTime();
startGate.countDown();
endGate.await();
long end = System.nanoTime();
return end - start;
}
}
5.5.2 FutureTask
public class Preloader {
private final FutureTask<ProductInfo> future = new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
@Override
public ProductInfo call() throws Exception {
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start() {
thread.start();
}
public ProductInfo get() {
try {
return future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException) {
throw (DataLoadException) cause;
} else {
throw new RuntimeException(e);
}
}
}
}
5.5.3 信号量
程序清单 5-5-3-1 使用Semaphore为容器设置边界
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Semaphore;
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore semaphore;
public BoundedHashSet(int bound) {
this.set = Collections.synchronizedSet(new HashSet<>());
semaphore = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
semaphore.acquire();
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
} finally {
if (!wasAdded) {
semaphore.release();
}
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved) {
semaphore.release();
}
return wasRemoved;
}
}
5.5.4 栅栏
程序清单 5-5-4-1 通过CyclicBarrier协调细胞自动衍生系统中的计算
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 工作线程都为各自子问题中的所有细胞计算新值。当所有的工作线程都到达栅栏时,
* 栅栏会把这些新值提交给数据模型。在栅栏的操作执行完以后,工作线程将开始下
* 一步的计算。
*/
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() {
@Override
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));
}
}
public void start() {
for (int i = 0; i < workers.length; i++) {
new Thread(workers[i]).start();
}
mainBoard.waitForConvergence();
}
private class Worker implements Runnable {
private final Board board;
public Worker(Board board) {
this.board = board;
}
@Override
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 e) {
throw new RuntimeException(e);
} catch (BrokenBarrierException e) {
throw new RuntimeException(e);
}
}
}
}
}
5.6 构建高效可伸缩的结果缓存
程序清单 5-6-1 缓存的最终实现
public interface Computable<A, V> {
V compute(A arg) throws InterruptedException;
}
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class Memoizer<A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
@Override
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>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (ExecutionException e) {
throw launcherThrowable(e.getCause());
}
}
}
}
import java.math.BigInteger;
@TreadSafe
public class Factorizer implements Servlet {
private final Computable<BigInteger, BigInteger[]> c = new Computable<BigInteger, BigInteger[]>() {
@Override
public BigInteger[] compute(BigInteger arg) throws InterruptedException {
return factor(arg);
}
};
private final Computable<BigInteger, BigInteger[]> cache = new Memoizer<>(c);
public void service(ServletRequest req, ServletResponse resp) {
try {
BigInteger i = extracFromRequest(req);
encodeToResponse(resp, cache.compute(i));
} catch (InterruptedException e) {
encodeError(resp, "factorization interrupted.");
}
}
}
基础知识小结
- 可变状态是至关重要的
所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。 - 尽量将域声明为final类型,除非需要它们是可变的。
- 不可变对象一定是线程安全的。
不可变对象能极大地降低并发编程复杂性。它们更为简单而且安全,可以任意共享而无需使用加锁或者保护性复制等机制。 - 封装有助于管理复杂性。
在编写线程安全的程序时,虽然可以将所有的数据都保存在全局变量中,但是为什么要这么做?将数据封装在对象中,更易于维持不变性条件:将同步机制封装在对象中,更易于遵循同步策略。 - 用锁来保护每个可变变量。
- 当保护同一个不变性条件中的所有变量时,要使用同一个锁。
- 在执行复合操作期间,要持有锁。
- 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
- 不要故作聪明地推断不需要使用同步
- 在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
- 将同步策略文档化。