第一章:简介
1.1 并发简史
促使进程出现的因素:资源利用率、公平性以及便利性等。这些因素同样也促使着线程的出现。线程允许同一个进程中同时存在多个程序控制流。线程会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有自己的程序计数器、栈以及局部变量等。在同一个程序中的多个线程也可以被同时调度到多个CPU上运行。
线程也被称为轻量级进程。线程是现代操作系统中基本的调度单位。
1.2 线程的优势
- 发挥多处理器的强大能力。(使用多个线程也有助于在单处理器系统上获得更高的吞吐率)
- 建模的简单性。
- 异步事件的简化处理。
- 响应更灵敏的用户界面。
1.3 线程带来的风险
- 安全性问题。
- 活跃性问题。
- 性能问题。
1.4 线程无处不在
框架通过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免地访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的。
第一部分:基础知识
第二章:线程安全性
如果当多个线程访问同一个可变的状态变量时没有使用合适的同步, 那么程序就会出现错误。有三种方式可以修复这个问题:
- 不在线程之间共享该状态变量。
- 将状态变量修改为不可变的变量。
- 在访问状态变量时使用同步。
在编写并发应用程序时,一种正确的编程方法就是:首先使代码正确运行,然后再提高代码的速度。
2.1 什么是线程安全性
线程安全性:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
在线程安全类中封装了必要的同步机制, 因此客户端无须进一步采取同步措施。
无状态的对象既不包含任何域,也不包含任何对其他类中域的引用。无状态对象一定是线程安全的。
2.2 原子性
竞态条件:由于不恰当的执行时序而出现不正确的结果。
竞态条件的类型之一,”先检查后执行”:首先观察到某个条件为真(例如文件X不存在),然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)。
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说都是原子的。原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
复合操作:包含了一组必须以原子方式执行的操作以确保线程安全性。
当在无状态的类中添加”一个“状态时,如果该状态完全由线程安全的对象来管理,那么这个类仍然是线程安全的。然而,当状态变量的数量由一个变为多个时,并不会像状态变量数量由零个变为一个那样简单。
在实际情况中,应尽可能地使用现有的线程安全对象(例如AtomicLong)来管理类的状态。与非线程安全的对象相比,判断线程安全对象的可能状态及其状态转换情况要更为容易,从而也更容易维护和验证线程安全性。
2.3 加锁机制
要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量。
内置锁:
synchronized (lock){
//访问或修改由锁保护的共享状态
}
重入:由于内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是“调用”。重入的一种实现方法是,为每个锁关联一个获取计数值和一个所有者线程。
2.4 用锁来保护状态
对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。
每个共享的和可变的变量都应该只由同一个锁来保护,从而使维护人员知道是哪一个锁。
对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。
虽然synchronized方法可以确保单个操作的原子性,但如果要把多个操作合并为一个复合操作,还是需要额外的加锁机制。
2.5 活跃性与性能
通常,在简单性与性能之间存在着相互制约因素。当实现某个同步策略时,一定不要盲目地为了性能而牺牲简单性(这可能会破坏安全性)。
当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络I/O或控制台I/O),一定不要持有锁。
第三章:对象的共享
3.1 可见性
在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。
只要有数据在多个线程之间共享,就使用正确的同步。
在多线程程序中使用共享且可变的long和double等类型的变量是不安全的(非原子的64位操作),除非用关键字volatile来声明它们,或者用锁保护起来。
加锁的含义不仅仅局限于互斥行为,还包括内在可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
从内存可见性的角度来看,写入volatile变量相当于退出同步代码块,而读取volatile变量就相当于进入同步代码块。
volatile boolean asleep;//当其他线程修改asleep时,执行判断的线程可及时发现
...
while(!asleep)
countSomeSheep();//数绵羊
调试小提示:对于服务器应用程序,无论在开发阶段还是在测试阶段,当启动JVM时一定都要指定-server命令行选项。server模式的JVM将比client模式的JVM进行更多的优化,例如将循环中未被修改的变量提升到循环外部,因此在开发环境(client模式的JVM)中能正确运行的代码,可能会在部署环境(server模式的JVM)中运行失败。例如,如果在以上程序中忘记把asleep变量声明为volatile类型,那么server模式的JVM会将asleep变量的判断条件提升到循环体外部(当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序),这将导致一个无限循环,但client模式的JVM不会这么做。在开发环境中出现无限循环问题时,解决这个问题的开销远小于解决在应用环境出现无限循环的开销。
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时, 才应该使用volatile变量:
- 对变量的写入操作不依赖于变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中。
- 在访问变量时不需要加锁。
3.2 发布与逸出
“发布(Publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。当某个不应该发布的对象被发布时,这种情况就被称为逸出(Escape)。
不要在构造过程中使this引用逸出。当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。
在构造过程中使this引用逸出的常见错误:
- 在构造函数中启动一个线程。
- 在构造函数中调用一个可改写的实例方法(既不是私有方法,也不是终结方法)。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。
/**
* ThisEscape
* <p/>
* Implicitly allowing the this reference to escape
*/
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
/**
* SafeListener
* <p/>
* Using a factory method to prevent the this reference from escaping during construction
*/
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
3.3 线程封闭
线程封闭:仅在单线程内访问数据(不共享数据)。
- Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担。
- 栈封闭:只能通过局部变量才能访问对象。
维持线程封闭性的一种更规范方法是使用ThreadLocal,这个类能使线程中的某个值与保存值的对象关联起来。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
3.4 不变性
不可变对象一定是线程安全的。
不可变性并不等于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型的,这个对象仍然是可变的,因为在final类型的域中可以保存对可变对象的引用。
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改。
- 对象的所有域都是final类型。
- 对象是正确创建的(在对象的创建期间,this引用没有逸出)。
正如“除非需要更高的可见性,否则应将所有的域都声明为私有域”是一个良好的编程习惯,“除非需要某个域是可变的,否则应将其声明为final域”也是一个良好的编程习惯。
每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。
对数值及其因数分解结果进行缓存的不可变容器类:
/**
* OneValueCache
* <p/>
* Immutable holder for caching a number and its factors
*/
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
使用指向不可变容器对象的volatile类型引用以缓存最新的结果:
/**
* VolatileCachedFactorizer
* <p/>
* Caching the last result using a volatile reference to an immutable holder object
* 当一个线程将volatile类型的cache设置为引用一个新的OneValueCache时,其他线程就会立即看到新缓存的数据
*/
@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
3.5 安全发布
/**
* Holder
* <p/>
* Class at risk of failure if not properly published
*/
public class Holder {
private int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
由于没有使用同步来确保Holder对象对其他线程可见,因此将Holder称为“未被正确发布”。在未被正确发布的对象中存在两个问题。首先,除了发布对象的线程外,其他线程可以看到的Holder域是一个失效值,因此将看到一个空引用或者之前的旧值。然而,更糟糕的情况是,线程看到Holder引用的值是最新的,但Holder状态的值却是失效的。情况变得更加不可预测的是,某个线程在第一次读取域时得到失效值,而再次读取这个域时会得到一个更新值,这也是assertSanity抛出AssertionError的原因。
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者AutomicReference对象中。
- 将对象的引用保存到某个正确构造对象的final类型域中。
- 将对象的引用保存到一个由锁保护的域中。
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder = new Holder(42);
静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。
如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象”。通过使用事实不可变对象,不仅可以简化开发过程,而且还能由于减少了同步而提高性能。在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。
对象的发布需求取决于它的可见性:
- 不可变对象可以通过任意机制来发布。
- 事实不可变对象必须通过安全方式来发布。
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
- 线程封闭。线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
- 只读共享。在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
- 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
- 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
第四章:对象的组合
4.1 设计线程安全的类
通过使用封装技术,可以使得在不对整个程序进行分析的情况下就可以判断一个类是否是线程安全的。
在设计一个线程安全类的过程中,需要包含以下三个基本要素:
- 找出构成对象状态的所有变量。
- 找出约束状态变量的不变性条件。
- 建立对象状态的并发访问管理策略。
4.2 实例封闭
将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。
封闭机制更易于构造线程安全的类,因为当封闭类的状态时,在分析类的线程安全性时就无需检查整个程序。
4.3 线程安全性的委托
如果一个类是由多个独立且线程安全的状态变量组成,并且在所有的操作中都不包含无效状态转换,那么可以将线程安全性委托给底层的状态变量。
/**
* VisualComponent
* <p/>
* Delegating thread safety to multiple underlying state variables
*/
public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();
public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}
public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}
}
/**
* NumberRange
* <p/>
* Number range class that does not sufficiently protect its invariants
*/
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException("can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException("can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
当把线程安全性委托给某个对象的底层状态变量时,在什么条件下才可以发布这些变量从而使其他类能修改它们?答案取决于在类中对这些变量施加了哪些不变性条件。如果一个状态变量是线程安全的,并且没有任何不变性条件来约束它的值,在变量的操作上也不存在任何不允许的状态转换,那么就可以安全地发布这个变量。
/**
* SafePoint
*/
@ThreadSafe
public class SafePoint {
@GuardedBy("this") private int x, y;
private SafePoint(int[] a) {
this(a[0], a[1]);
}
public SafePoint(SafePoint p) {
this(p.get());
}
public SafePoint(int x, int y) {
this.set(x, y);
}
public synchronized int[] get() {
return new int[]{x, y};
}
public synchronized void set(int x, int y) {
this.x = x;
this.y = y;
}
}
/**
* PublishingVehicleTracker
* <p/>
* Vehicle tracker that safely publishes underlying state
*/
@ThreadSafe
public class PublishingVehicleTracker {
private final Map<String, SafePoint> locations;
private final Map<String, SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String, SafePoint> locations) {
this.locations = new ConcurrentHashMap<String, SafePoint>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String, SafePoint> getLocations() {
return unmodifiableMap;
}
public SafePoint getLocation(String id) {
return locations.get(id);
}
public void setLocation(String id, int x, int y) {
if (!locations.containsKey(id))
throw new IllegalArgumentException("invalid vehicle name: " + id);
locations.getLocation(id).set(x, y);
}
}
4.4 在现有的线程安全类中添加功能
- 在原始类中添加一个方法。
- 对类进行扩展。
- 扩展类的功能,但并不是扩展类本身,而是将扩展代码放入一个“辅助类”中。
- 组合。
/**
* BetterVector
* <p/>
* Extending Vector to have a put-if-absent method
* 对类进行扩展
*/
@ThreadSafe
public class BetterVector <E> extends Vector<E> {
// When extending a serializable class, you should redefine serialVersionUID
static final long serialVersionUID = -3963416950630760754L;
public synchronized boolean putIfAbsent(E x) {
boolean absent = !contains(x);
if (absent)
add(x);
return absent;
}
}
/**
* ListHelper
* <p/>
* Examples of thread-safe and non-thread-safe implementations of
* put-if-absent helper methods for List
* 将扩展代码放入一个“辅助类”中
*/
@NotThreadSafe
class BadListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}//synchronized是辅助类的锁,在错误的锁上进行了同步
@ThreadSafe
class GoodListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
/**
* ImprovedList
*
* Implementing put-if-absent using composition
*
*/
@ThreadSafe
public class ImprovedList<T> implements List<T> {
private final List<T> list;
/**
* PRE: list argument is thread-safe.
*/
public ImprovedList(List<T> list) { this.list = list; }
public synchronized boolean putIfAbsent(T x) {
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
// Plain vanilla delegation for List methods.
// Mutative methods must be synchronized to ensure atomicity of putIfAbsent.
public int size() {
return list.size();
}
public boolean isEmpty() {
return list.isEmpty();
}
public boolean contains(Object o) {
return list.contains(o);
}
public Iterator<T> iterator() {
return list.iterator();
}
public Object[] toArray() {
return list.toArray();
}
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
public synchronized boolean add(T e) {
return list.add(e);
}
public synchronized boolean remove(Object o) {
return list.remove(o);
}
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
public synchronized boolean addAll(Collection<? extends T> c) {
return list.addAll(c);
}
public synchronized boolean addAll(int index, Collection<? extends T> c) {
return list.addAll(index, c);
}
public synchronized boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
public synchronized boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
public boolean equals(Object o) {
return list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public T get(int index) {
return list.get(index);
}
public T set(int index, T element) {
return list.set(index, element);
}
public void add(int index, T element) {
list.add(index, element);
}
public T remove(int index) {
return list.remove(index);
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public ListIterator<T> listIterator() {
return list.listIterator();
}
public ListIterator<T> listIterator(int index) {
return list.listIterator(index);
}
public List<T> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
public synchronized void clear() { list.clear(); }
}
4.5 将同步策略文档化
在文档中说明客户代码需要了解的线程安全性保证,以及代码维护人员需要了解的同步策略。
第五章:基础构建模块
委托是创建线程安全类的一个最有效的策略:只需让现有的线程安全类管理所有的状态即可。
5.1 同步容器类
同步容器类包括Vector和Hashtable,还包括一些由Collections.syschronizedXxx等工厂方法创建的同步的封装器类。这些类实现线程安全的方式是:将它们的状态封装起来,并对每个公有方法都进行同步,使得每次只有一个线程能访问容器的状态。
同步容器类都是线程安全的,但在某些情况下可能需要额外的客户端加锁来保护复合操作。容器上常见的复合操作包括:迭代(反复访问元素,直到遍历完容器中所有元素)、跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算(例如“若没有则添加”)。
//Vector上可能导致混乱结果的复合操作
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);
}
//在使用客户端加锁的Vector上的复合操作
public static Object getLast(Vector list){
syschronized(list){
int lastIndex = list.size() - 1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list){
syschronized(list){
int lastIndex = list.size() - 1;
list.remove(lastIndex);
}
}
在设计同步容器类的迭代器时并没有考虑到并发修改的问题,并且它们表现出的行为是“及时失败”(fail-fast)的。这意味着,当它们发现容器在迭代过程中被修改时,就会抛出一个ConcurrentModificationException异常。
这种“及时失败”的迭代器并不是一种完备的处理机制,而只是“善意地”捕获并发错误,因此只能作为并发问题的预警指示器。它们采用的实现方式是,将计数器的变化与容器关联起来:如果在迭代期间计数器被修改,那么hasNext或next将抛出ConcurrentModificationException。然而,这种检查是在没有同步的情况下进行的,因此可能会看到失效的计数器,而迭代器可能并没有意识到已经发生了修改。这是一种设计上的权衡,从而降低并发修改操作的检测代码对程序性能带来的影响。(在单线程代码中也可能抛出ConcurrentModificationException异常,当对象直接从容器中删除而不是通过Iterator.remove来删除时)
如果不希望在迭代期间对容器加锁,那么一种替代方法就是“克隆”容器(在克隆过程中仍然需要对容器加锁),并在副本上进行迭代。在克隆容器时存在显著的性能开销。这种方式的好坏取决于多个因素,包括容器的大小,在每个元素上执行的工作,迭代操作相对于容器其他操作的调用频率,以及在响应时间和吞吐量等方面的需求。
正如封装对象的状态有助于维持不变性条件一样,封装对象的同步机制同样有助于确保实施同步策略。
容器的hashCode和equals等方法也会间接地执行迭代操作,当容器作为另一个容器的元素或键值时,就会出现这种情况。同样,containsAll、removeAll和retainAll等方法,以及把容器作为参数的构造函数,都会对容器进行迭代。所有这些间接的迭代操作都可能抛出ConcurrentModificationException。
5.2 并发容器
同步容器将所有对容器状态的访问都串行化,以实现它们的线程安全性,这种方法的代价是严重降低并发性。并发容器则是针对多个线程并发访问设计的。
通过并发容器来代替同步容器,可以极大地提高伸缩性并降低风险。
ConcurrentHashMap:
ConcurrentHashMap与其他并发容器一起增强了同步容器类:它们提供的迭代器不会抛出ConcurrentModificationException,因此不需要在迭代过程中对容器加锁。ConcurrentHashMap返回的迭代器具有弱一致性,而并非“及时失败”。弱一致性的迭代器可以容忍并发的修改,当创建迭代器时会遍历已有的元素,并可以(但是不保证)在迭代器被构造后将修改操作反映给容器。
尽管有这些改进,但仍然有一些需要权衡的因素。对于一些需要在整个Map上进行计算的方法,例如size和isEmpty,这些方法的语义被略微减弱了以反映容器的并发特性(例如允许size返回一个近似值而不是一个精确值)。size和isEmpty这样的方法在并发环境下的用处很小,因为它们的返回值总在不断变化。因此,这些操作的需求被弱化了,以换取对其他更重要操作的性能优化,包括put、get、containsKey和remove等。
与Hashtable和syschronizedMap相比,ConcurrentHashMap有着更多的优势以及更少的劣势,因此在大多数情况下,用ConcurrentHashMap来代替同步Map能进一步提高代码的可伸缩性。只有当应用程序需要加锁Map以进行独占访问(或者需要依赖于同步Map带来的一些其他作用)时,才应该放弃使用ConcurrentHashMap。
由于ConcurrentHashMap不能被加锁来执行独占访问,因此我们无法使用客户端加锁来创建新的原子操作。但是,一些常见的复合操作,例如“若没有则添加”、“若相等则移除”和“若相等则替换”等,都已经实现为原子操作并且在ConcurrentMap接口中声明。如果你需要在现有的同步Map中添加这样的功能,那么很可能就意味着应该考虑使用ConcurrentMap了。
//ConcurrentMap接口
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);
}
CopyOnWriteArrayList:
CopyOnWriteArrayList用来替代同步List,在某些情况下它提供了更好的并发性能,并且在迭代期间不需要对容器进行加锁或复制。类似地,CopyOnWriteArraySet的作用是替代同步Set。
“写入时复制”容器的线程安全性在于,只要正确地发布一个事实不可变的对象,那么在访问该对象时就不再需要进一步的同步。在每次修改时,都会创建并重新发布一个新的容器副本,从而实现可变性。
显然,每当修改容器时都会复制底层数组,这需要一定的开销,特别是当容器的规模较大时。仅当迭代操作远远多于修改操作时,才应该使用“写入时复制”容器。
5.3 阻塞队列和生产者-消费者模式
阻塞队列提供了可阻塞的put和take方法,以及支持定时的offer和poll方法。如果队列已经满了,那么put方法将阻塞直到有空间可用;如果队列为空,那么take方法将会阻塞直到有元素可用。队列可以是有界的也可以是无界的,无界队列永远都不会充满。
在基于阻塞队列构建的生产者-消费者设计中,当数据生成时,生产者将数据放入队列,而当消费者准备处理数据时,将从队列中获取数据。BlockingQueue简化了生产者-消费者设计的实现过程,它支持任意数量的生产者和消费者。
阻塞队列同样提供了一个offer方法,如果数据项不能被添加到队列中,那么将返回一个失败状态。这样你就能够创建更多灵活的策略来处理负荷过载的情况。
在构建高可靠的应用程序时,有界队列是一种强大的资源管理工具:它们能抑制并防止产生过多的工作项,使应用程序在负荷过载的情况下变得更加健壮。
在类库中包含了BlockingQueue的多种实现。其中,LinkedBlockingQueue和ArrayBlockingQueue是FIFO队列;PriorityBlockingQueue是一个按优先级排序的队列,既可以根据元素的自然顺序来比较元素(如果它们实现了Comparable方法),也可以使用Comparator来比较;最后一个BlockingQueue实现是SyschronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间,它维护一组线程,这些线程在等待着把元素加入或移出队列。这种实现队列的方式可以直接交付工作。仅当有足够多的消费者,并且总是有一个消费者准备好获取交付的工作时,才适合使用同步队列。
Java 6 增加了两种容器类型,Deque 和 BlockingDeque ,它们分别对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingQueue。正如阻塞队列适用于生产者-消费者模式,双端队列同样适用于另一种相关模式,即工作密取。在生产者-消费者设计中,所以消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列(极大地减少了竞争)。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列的末尾(从尾部而不是头部获取工作,进一步降低了队列上的竞争程度)秘密地获取工作。工作密取非常适用于既是消费者也是生产者问题—当执行某个工作时可能导致出现更多的工作。例如,在网页爬虫程序中处理一个页面时,通常会发现有更多的页面需要处理。当一个工作线程找到新的任务单元时,它会将其放到自己队列的末尾(或者在工作共享设计模式中,放入其他工作者线程的队列中)。当双端队列为空时,它会在另一个线程的队列队尾查找新的任务,从而确保每个线程都保持忙碌状态。
5.4 阻塞方法与中断方法
线程阻塞或暂停的原因:等待I/O操作结束,等待获得一个锁,等待从Thread.sleep方法中醒来,或是等待另一个线程的计算结果。
当某方法抛出InterruptedException时,表示该方法是一个阻塞方法,如果这个方法被中断,那么它将努力提前结束阻塞状态。
Thread提供了interrupt方法,用于中断线程或者查询线程是否已经被中断。每个线程都有一个布尔类型的属性,表示线程的中断状态,当中断线程时将设置这个状态。
中断是一种协作机制。一个线程不能强制其他线程停止正在执行的操作而去执行其他的操作。当线程A中断B时,A仅仅是要求B在执行到某个可以暂停的地方停止正在执行的操作——前提是如果线程B愿意停止下来。最常使用中断的情况就是取消某个操作。方法对中断请求的响应度越高,就越容易及时取消那些执行时间很长的操作。
处理对中断的响应:
- 传递InterruptedException。避开这个异常通常是最明智的策略——只需把InterruptedException传递给方法的调用者。
- 恢复中断。有时候不能抛出InterruptedException,例如当代码是Runnable的一部分时。在这些情况下必须捕获InterruptedException,并通过调用当前线程上的interrupt方法恢复中断状态,这样在调用栈中更高层的代码将看到引发了一个中断。
/**
* TaskRunnable
* <p/>
* Restoring the interrupted status so as not to swallow the interrupt
*/
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 {
}
}
在出现InterruptedException时不应该做的事情是,捕获它但不做出任何响应。只有在一种特殊的情况下才能屏蔽中段,即对Thread进行扩展,并且能控制调用栈上所有更高层的代码。
5.5 同步工具类
同步工具类:阻塞队列、信号量(Semaphore)、栅栏(Barrier)以及闭锁(Latch)等。
所有的同步工具类都包含一些特定的结构化属性:它们封装了一些状态,这些状态将决定执行同步工具类的线程是继续执行还是等待,此外还提供了一些方法对状态进行操作,以及另一些方法用于高效地等待同步工具类进入到预期状态。
闭锁:
闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当到达结束状态时,这扇门会打开并允许所有的线程通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。闭锁可以用来确保某些活动直到其他活动都完成后才继续执行。
1.CountDownLatch:
一种灵活的闭锁实现,闭锁状态包括一个计数器,该计数器被初始化为一个正数,表示需要等待的事件数量。countDown方法递减计数器,表示有一个事件已经发生了,而await方法等待计数器达到零,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么await会一直阻塞直到计数器达到零,或者等待中的线程中断,或者等待超时。
/**
* TestHarness
* <p/>
* Using CountDownLatch for starting and stopping threads in timing tests
*/
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;
}
}
启动门将使得主线程能够同时释放所有工作线程,而结束门则使主线程能够等待最后一个线程执行完成。
2.FutureTask:
FutureTask实现了Future语义,表示一种抽象的可生成结果的计算。FutureTask表示的计算是通过Callable来实现的,相当于一种可生成结果的Runnable,并且可以处于以下三种状态:等待运行、正在运行和运行完成。“执行完成”表示计算的所有可能结束方式,包括正常结束、由于取消而结束和由于异常而结束等。当FutureTask进入完成状态后,它会永远停止在这个状态上。
Future.get的行为取决于任务的状态。如果任务已经完成,那么get会立即返回结果,否则get将阻塞直到任务进入完成状态,然后返回结果或者抛出异常。FutureTask将计算结果从执行计算的线程传递到获取这个结果的线程,而FutureTask的规范确保了这种传递过程能实现结果的安全发布。
FutureTask在Executor框架中表示异步任务,此外还可以表示一些时间较长的计算,这些计算可以在使用计算结果之前启动。通过提前启动计算,可以减少在等待结果时需要的时间。
/**
* Preloader
*
* Using FutureTask to preload data that is needed later
*/
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
*
*/
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);
}
}
信号量:
计数信号量用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。计数信号量还可以用来实现某种资源池,或者对容器施加边界。
Semaphore中管理着一组虚拟的许可,许可的初始数量可通过构造函数来指定。在执行操作时首先获得许可(只要还有剩余的许可),并在使用以后释放许可。如果没有许可,那么acquire将阻塞直到有许可(或者直到被中断或者操作超时)。release方法将返回一个许可给信号量。在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为消费一个许可,release操作创建一个许可。Semaphore并不受限于它在创建时的初始许可数量。
计算信号量的一种简化形式是二值信号量,即初始值为1的Semaphore。二值信号量可以用作互斥体(mutex),并具备不可重入的加锁语义:谁拥有这个唯一的许可,谁就拥有了互斥锁。
/**
* BoundedHashSet
* <p/>
* Using Semaphore to bound a collection
* 使用Semaphore为容器设置边界
*/
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();//add操作不成功则释放许可
}
}
public boolean remove(Object o) {
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}
栅栏:
栅栏类似于闭锁,它能阻塞一组线程直到某个事件发生。栅栏与闭锁的关键区别在于,所有线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
1.CyclicBarrier
可以使一定数量的参与方反复地在栅栏位置汇集,它在并行迭代算法中非常有用:这种算法通常将一个问题拆分为一系列相互独立的子问题。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达了栅栏位置,那么栅栏将打开,此时所有线程都被释放,而栅栏将被重置以便下次使用。如果对await的调用超时,或者await的阻塞线程被中断,那么栅栏就被认为是打破了,所有阻塞的await调用都将终止并抛出BrokenBarrierException。如果成功地通过栅栏,那么await将为每个线程返回一个唯一的到达索引号,我们可以利用这些索引来”选举“产生一个领导线程,并在下一次迭代中由该领导线程执行一些特殊的工作。CyclicBarrier还可以使你将一个栅栏操作传递给构造函数,这是一个Runnable,当成功通过栅栏时会(在一个子任务线程中)执行它,但在阻塞线程被释放之前是不能被执行的。
2.Exchanger
一种两方栅栏,各方在栅栏位置上交换数据。当两方执行不对称的操作时,Exchanger会非常有用,例如当一个线程向缓冲区写入数据,而另一个线程从缓冲区中读取数据。
5.6 构建高效且可伸缩的结果缓存
interface Computable <A, V> {
V compute(A arg) throws InterruptedException;
}
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());
}
}
}
}
仍然存在一些问题:缓存污染(缓存的是Future而不是值,可能计算被取消或失败);缓存逾期;缓存清理。
/**
* Factorizer
* <p/>
* Factorizing servlet that caches results using Memoizer
*/
@ThreadSafe
public class Factorizer extends GenericServlet implements Servlet {
private final Computable<BigInteger, BigInteger[]> c =
new Computable<BigInteger, BigInteger[]>() {
public BigInteger[] compute(BigInteger arg) {
return factor(arg);
}
};
private final Computable<BigInteger, BigInteger[]> cache
= new Memoizer<BigInteger, BigInteger[]>(c);
public void service(ServletRequest req,
ServletResponse resp) {
try {
BigInteger i = extractFromRequest(req);
encodeIntoResponse(resp, cache.compute(i));
} catch (InterruptedException e) {
encodeError(resp, "factorization interrupted");
}
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
void encodeError(ServletResponse resp, String errorString) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}
第一部分小结
- 可变状态是至关重要的。
所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性。 - 尽量将域声明为final类型,除非需要它们是可变的。
- 不可变对象一定是线程安全的。
不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。 - 封装有助于管理复杂性。
在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中,但为什么要这么做?将数据封装在对象中,更易于维持不变性条件;将同步机制封装在对象中,更易于遵循同步策略。 - 用锁来保护每个可变变量。
- 当保护同一个不变性条件中的所有变量时,要使用同一个锁。
- 在执行复合操作期间,要持有锁。
- 如果从多个线程中访问同一个可变变量时没有同步机制,那么程序会出现问题。
- 不要故作聪明地推断出不需要使用同步。
- 在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
- 将同步策略文档化。
第二部分:结构化并发应用程序
第六章:任务执行
6.1 在线程中执行任务
大多数服务器应用程序都提供了一种自然的任务边界选择方式:以独立的客户请求为边界。
任务执行策略:
1.串行地执行任务:通常无法提供高吞吐率或快速响应性。
2.为每个请求创建一个新的线程:线程生命周期的开销非常高;资源消耗;稳定性。在一定的范围内,增加线程可以提高系统的吞吐率,但如果超出了这个范围,再创建更多的线程只会降低程序的执行速度,并且如果过多地创建线程,那么整个应用程序将崩溃。
6.2 Executor框架
//Executor接口
public interface Executor {
void execute(Runnable command);
}
该框架能支持多种不同类型的任务执行策略。它提供了一种标准的方法将任务的提交过程与执行过程解耦开来,并用Runnable来表示任务。
Executor基于生产者-消费者模式,提交任务的操作相当于生产者,执行任务的线程则相当于消费者。
/**
* TaskExecutionWebServer
* <p/>
* Web server using a thread pool
*/
public class TaskExecutionWebServer {
private static final int NTHREADS = 100;
private static final Executor exec
= Executors.newFixedThreadPool(NTHREADS);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
};
exec.execute(task);
}
}
private static void handleRequest(Socket connection) {
// request-handling logic here
}
}
/**
* ThreadPerTaskExecutor
* <p/>
* Executor that starts a new thread for each task
*/
public class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
};
}
/**
* WithinThreadExecutor
* <p/>
* Executor that executes tasks synchronously in the calling thread
*/
public class WithinThreadExecutor implements Executor {
public void execute(Runnable r) {
r.run();
};
}
各种执行策略都是一种资源管理工具,最佳策略取决于可用的计算资源以及对服务质量的需求。通过将任务的提交与任务的执行策略分离开来,有助于在部署阶段选择与可用硬件资源最匹配的执行策略。
每当看到下面这种形式的代码时:
new Thread(runnable).start();
并且你希望获得一种更灵活的执行策略时,请考虑使用Executor来代替Thread。
线程池:
“在线程池中执行任务”比“为每个任务分配一个线程”优势更多。通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态,同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。
可以通过调用Executors中的静态工厂方法之一来创建一个线程池:
newFixedThreadPool:newFixedThreadPool将创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,直到达到线程池的最大数量,这时线程池的规模将不再发生变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池将补充一个新的线程)。
newCachedThreadPool:newCachedThreadPool将创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,而当需求增加时,则可以添加新的线程,线程池的规模不存在任何限制。
newSingleThreadExecutor:newSingleThreadExecutor是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。newSingleThreadExecutor能确保依照任务在队列中的顺序来串行执行(例如FIFO、LIFO、优先级)。单线程的Executor还提供了大量的内部同步机制,从而确保了任务执行的任何内存写入操作对于后续任务来说都是可见的。这意味着,即使这个线程会不时地被另一个线程替代,但对象总是可以安全地封闭在“任务线程”中。
newScheduledThreadPool:newScheduledThreadPool创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
Timer支持基于绝对时间而不是相对时间的调度机制,因此任务的执行对系统时钟变化很敏感,而ScheduledThreadPoolExecutor只支持基于相对时间的调度。Timer在执行所有定时任务时只会创建一个线程,如果某个任务的执行时间过长,那么将会破坏其他TimerTask的定时精确性。Timer的另一个问题是,如果TimerTask抛出了一个未检查的异常,那么Timer将表现出糟糕的行为。Timer线程并不捕获异常,因此当TimerTask抛出未检查的异常时将终止定时线程。这种情况下,Timer也不会恢复线程的执行,而是会错误地认为整个Timer都被取消了。这个问题称之为“线程泄露”。在Java 5.0 或更高的JDK 中,将很少使用Timer。
如果要构建自己的调度服务,那么可以使用DelayQueue,它实现了BlockingQueue,并为ScheduledThreadPoolExecutor提供调度功能。DelayQueue管理着一组Delayed对象。每个Delayed对象都有一个相应的延迟时间:在DelayQueue中,只有某个元素逾期后,才能从DelayQueue中执行take操作。从DelayQueue中返回的对象将根据它们的延迟时间进行排序。
Executor执行的任务有4个生命周期阶段:创建、提交、开始和完成。由于有些任务可能要执行很长的时间,因此通常希望能够取消这些任务。在Executor框架中,已提交但尚未开始的任务可以取消,但对于那些已经开始执行的任务,只有当它们能响应中断时才能取消。取消一个已经完成的任务没有任何影响。
Future表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。在Future规范中包含的隐含意义是,任务的生命周期只能前进,不能后退。当某个任务完成后,它就永远停留在“完成”状态上。get方法取决于任务的状态(尚未开始、正在运行、已完成)。如果任务已经完成,那么get会立即返回或者抛出一个Exception;如果任务没有完成,那么get将阻塞并直到任务完成。如果任务抛出了异常,那么get将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get将抛出CancellationException。如果get抛出了ExecutionException,那么可以通过getCause来获得被封装的初始异常。
PS:
Callable 和 Runnable 的使用方法大同小异, 区别在于:
1.Callable 使用 call() 方法, Runnable 使用 run() 方法
2.call() 可以返回值, 而 run()方法不能返回。
3.call() 可以抛出受检查的异常,比如ClassNotFoundException, 而run()不能抛出受检查的异常。
4.ExecutorService 在Callable中使用的是submit(), 在Runnable中使用的是 execute()。
CompletionService:
将Executor和BlockingQueue的功能融合在一起。你可以将Callable任务提交给它来执行,然后使用类似于队列操作的take和poll等方法来获得已完成的结果,而这些结果会在完成时被封装为Future。ExecutorCompletionService实现了CompletionService,并将计算部分委托给一个Executor。
/**
* Renderer
* <p/>
* Using CompletionService to render page elements as they become available
*/
public abstract class Renderer {
private final ExecutorService executor;
Renderer(ExecutorService executor) {
this.executor = executor;
}
void renderPage(CharSequence source) {
final List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService =
new ExecutorCompletionService<ImageData>(executor);
for (final ImageInfo imageInfo : info)
completionService.submit(new Callable<ImageData>() {
public ImageData call() {
return imageInfo.downloadImage();
}
});
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++) {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
interface ImageData {
}
interface ImageInfo {
ImageData downloadImage();
}
abstract void renderText(CharSequence s);
abstract List<ImageInfo> scanForImageInfo(CharSequence s);
abstract void renderImage(ImageData i);
}
多个ExecutorCompletionService可以共享一个Executor,因此可以创建一个对于特定计算私有,又能共享一个Executor的ExecutorCompletionService。
有时候,如果某个任务无法在指定时间内完成, 那么将不再需要它的结果,此时可以放弃这个任务。在支持时间限制的Future.get中:当结果可用时,它将立即返回;如果在指定时限内没有计算出结果,那么将抛出TimeoutException。在使用限时任务时需要注意,当这些任务超时后应该立即停止,从而避免为继续计算一个不再使用的结果而浪费计算资源。传递给get的timeout参数的计算方法是,将指定时限减去当前时间,这可能会得到负数,但java.util.concurrent中所有与时限相关的方法都将负数视为零,因此不需要额外的代码来处理这种情况。
/**
* RenderWithTimeBudget
*
* Fetching an advertisement with a time budget
*/
public class RenderWithTimeBudget {
private static final Ad DEFAULT_AD = new Ad();
private static final long TIME_BUDGET = 1000;
private static final ExecutorService exec = Executors.newCachedThreadPool();
Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> f = exec.submit(new FetchAdTask());
// Render the page while waiting for the ad
Page page = renderPageBody();
Ad ad;
try {
// Only wait for the remaining time budget
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFAULT_AD;
} catch (TimeoutException e) {
ad = DEFAULT_AD;
f.cancel(true);
}
page.setAd(ad);
return page;
}
Page renderPageBody() { return new Page(); }
static class Ad {
}
static class Page {
public void setAd(Ad ad) { }
}
static class FetchAdTask implements Callable<Ad> {
public Ad call() {
return new Ad();
}
}
}
invokeAll:支持限时,将多个任务提交到一个ExecutorService并获得结果。invokeAll方法的参数为一组任务,并返回一组Future。这两个集合有着相同的结构。invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回的集合中,从而使调用者能将各个Future与其表示的Callable关联起来。当所有任务都执行完毕时,或者调用线程被中断时,又或者超过指定时限时,invokeAll将返回。当超过指定时限后,任何还未完成的任务都会取消。当invokeAll返回后,每个任务要么正常地完成,要么被取消,而客户端代码可以调用get或isCancelled来判断究竟是何种情况。
/**
* 引自:http://zld406504302.iteye.com/blog/1840091
*10个班级,每个班级20名学生,在指定的时间内查询每个班级学生的集合。
*/
public class FutureTest {
//缓存操作数据集
private static final Map<Integer, List<Student>> sutdenMap = new HashMap<Integer, List<Student>>();
//初始化操作数据
static {
List<Student> stuList = null;
Student stu;
for (int i = 0; i < 10; i++) {
stuList = new ArrayList<Student>();
for (int j = 0; j < 20; j++) {
stu = new Student(j, "zld_" + i + "." + j, i);
stuList.add(stu);
}
sutdenMap.put(i, stuList);
}
}
public static class Student {
private int id;
private String name;
private int classID;
public Student(int id, String name, int classID) {
this.id = id;
this.name = name;
this.classID = classID;
}
public String toString() {
return Student.class.getName() + "(id:" + this.id + ",name:"
+ this.name + ")";
}
}
/**
* @filename: SearchTask
* @description: 查询任务
* @author lida
* @date 2013-4-1 下午3:02:29
*/
public static class SearchTask implements Callable<List<Student>> {
public final int classID;
public long sleepTime;
/**
* <p>Title: </p>
* <p>Description: </p>
* @param classID 班级编号
* @param sleepTime 模拟操作所用的时间数(毫秒)
*/
SearchTask(int classID, long sleepTime) {
this.classID = classID;
this.sleepTime = sleepTime;
}
@Override
public List<Student> call() throws Exception {
//模拟操作所用的时间数(毫秒)
Thread.sleep(sleepTime);
List<Student> stuList = sutdenMap.get(classID);
return stuList;
}
}
public static void main(String[] args) {
FutureTest ft = new FutureTest();
ExecutorService exec = Executors.newCachedThreadPool();
List<SearchTask> searchTasks = new ArrayList<SearchTask>();
SearchTask st;
for (int i = 0; i < 10; i++) {
st = new SearchTask(i, 2001);//指定2000毫秒为最大执行时间
searchTasks.add(st);
}
try {
//要求在2000毫秒内返回结果,否则取消执行。
List<Future<List<Student>>> futures = exec.invokeAll(searchTasks,
2000, TimeUnit.MILLISECONDS);//invokeAll 第一个参数是任务列表;第二个参数是过期时间;第三个是过期时间单位
for (Future<List<Student>> future : futures) {
List<Student> students = future.get();
for (Student student : students) {
System.out.println(student.toString());
}
}
exec.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
Thread.interrupted();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
第七章:取消与关闭
7.1 任务取消
如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为“可取消的”。
取消某个操作的原因:
- 用户请求取消。
- 有时间限制的操作。
- 应用程序事件。例如,应用程序对某个问题空间进行分解并搜索,从而使不同的任务可以搜索问题空间中的不同区域。当其中一个任务找到了解决方案时,所有其他仍在搜索的任务都将被取消。
- 错误。网页爬虫程序搜素相关的页面,并将页面或摘要数据保存到硬盘。当一个爬虫任务发生错误时(例如,磁盘空间已满),那么所有搜索任务都会取消。此时可能会记录它们的当前状态,以便稍后稍后重新启动。
- 关闭。当一个程序或服务关闭时,必须对正在处理和等待处理的工作执行某种操作。在平缓的关闭过程中,当前正在执行的任务将继续执行直到完成;而在立即关闭过程中,当前的任务则可能取消。
Java中没有一种安全的抢占式方式来停止线程,因此也就没有安全的抢占式方式来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。
/**
* PrimeGenerator
* <p/>
* Using a volatile field to hold cancellation state
*/
@ThreadSafe
public class PrimeGenerator implements Runnable {
private static ExecutorService exec = Executors.newCachedThreadPool();
@GuardedBy("this") private final List<BigInteger> primes
= new ArrayList<BigInteger>();
private volatile boolean cancelled;
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {
cancelled = true;
}
public synchronized List<BigInteger> get() {
return new ArrayList<BigInteger>(primes);
}
static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
exec.execute(generator);
try {
SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}
}
一个可取消的任务必须拥有取消策略:其他代码如何(How)请求取消该任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行哪些(What)操作。
在Java的API或语言规范中,并没有将中断与任何取消语义关联起来,但实际上,如果在取消之外的其他操作中使用中断,那么都是不合适的,并且很难支撑起更大的应用。
每个线程都有一个boolean类型的中断状态。当中断线程时,这个线程的中断状态将被设置为true。在Thread中包含了中断线程以及查询线程中断状态的方法,interrupt方法能中断目标线程,isInterrupted方法能返回目标线程的中断状态,静态的interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方法。
public class Thread{
public void interrupt() {...}
public boolean isInterrupted() {...}
public static boolean interrupted() {...}
}
调用interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。
对中断操作的正确理解是:它并不会真正地中断一个正在运行的线程,而只是发出中断请求,然后由线程在下一个合适的时刻中断自己。(这些时刻也被称为取消点)
在使用静态的interrupted时应该非常小心,因为它会清除当前线程的中断状态。如果在调用interrupted时返回了true,那么除非你想屏蔽这个中断,否则必须对它进行处理——可以抛出InterruptedException,或者通过再次调用interrupt来恢复中断状态。
通常,中断是实现取消的最合理方式。
区分任务和线程对中断的反应是很重要的。任务不会在其自己拥有的线程中执行,而是在某个服务(例如线程池)拥有的线程中执行。对于非线程所有者的代码来说,应该小心地保存中断状态,这样拥有线程的代码才能对中断做出响应,即使“非所有者”代码也可以做出响应。这就是为什么大多数可阻塞的库函数都只是抛出InterruptedException作为中断响应:尽快退出执行流程,并把中断信息传递给调用者,从而使调用栈中的上层代码可以采取进一步的操作。如果除了将InterruptedException传递给调用者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:
Thread.currentThread().interrupt;
线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如关闭(shutdown)方法。
由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。
只有实现了线程中断策略的代码才可以屏蔽中断请求,在常规的任务和库代码中都不应该屏蔽中断请求。
通常,可中断的方法会在阻塞或进行重要的工作前首先检查中断,从而尽快地响应中断。
通过Future来实现取消:
ExecutorService.submit将返回一个Future来描述任务。Future拥有一个cancel方法,该方法带有一个boolean类型的参数mayInterruptIfRunning,表示取消操作是否成功。(这只是表示任务是否能够接收中断,而不是表示任务是否能检测并处理中断。)如果mayInterruptIfRunning为true并且任务当前正在某个线程中运行,那么这个线程能被中断。如果这个参数为false,那么意味着“若任务还没有启动,就不要运行它”,这种方式应该用于那些不处理中断的任务中。
当尝试取消某个任务时,不宜直接中断线程池,因为你并不知道当中断请求到达时正在运行什么任务——只能通过任务的Future来实现取消。
//通过Future来取消任务
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
Future<?> task = taskExec.submit(r);
try{
task.get(timeout, unit);
}catch(TimeoutException e) {
//接下来任务将被取消
}catch(ExecutionException e) {
//如果在任务中抛出了异常,那么重新抛出该异常
throw launderThrowable(e.getCause());
}finally{
//如果任务已经结束,那么执行取消操作也不会带来任何影响
task.cancel(true);//如果任务正在进行,那么将被中断
}
}
当Future.get抛出InterruptedException或TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。
正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。线程有一个相应的所有者,即创建该线程的类。
与其他封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。
对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。例如,在ExecutorService中提供了shutdown和shutdownNow等方法。
当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器,那么默认的行为是将栈追踪信息输出到System.error。
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
异常处理器如何处理未捕获异常,取决于对服务质量的需求。最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序日志中。异常处理器还可以采取更直接的响应,例如尝试重新启动线程,关闭应用程序,或者执行其他修复或诊断等操作。
/**
* UEHLogger
* <p/>
* UncaughtExceptionHandler that logs the exception
*/
public class UEHLogger implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
}
}
在运行时间较长的应用程序中,通常会为所有线程的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。
与所有的线程操控一样,只有线程的所有者能够改变线程的UncaughtExceptionHandler。
如果你希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或Callable中,或者改写ThreadPoolExecutor的afterExecute方法。
只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出了异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。
7.4 JVM关闭
JVM既可以正常关闭,也可以强行关闭。正常关闭的触发方式包括:当最后一个“正常(非守护)”线程结束时,或者当调用了System.exit时,或者通过其它特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-C)。强行关闭包括:通过调用Runtime.halt或者在操作系统中“杀死”JVM进程(例如发送SIGKILL)。
关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程。关闭钩子可以用于实现服务或应用程序的清理工作,例如删除临时文件,或者清除无法由操作系统自动清除的资源。
由于关闭钩子将并发执行,因此在关闭日志文件时可能导致其他需要日志服务的关闭钩子产生问题。为了避免这种情况,关闭钩子不应该依赖于那些可能被应用程序或其他关闭钩子关闭的服务。实现这种功能的一种方式是对所有服务使用同一个关闭钩子,而不是每个服务使用一个不同的关闭钩子,并且在该关闭钩子中执行一系列的关闭操作。这确保了关闭操作在单个线程中串行执行,从而避免了在关闭操作之间出现竞态条件或死锁等问题。
//通过注册一个关闭钩子来停止日志服务
public void start() {
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
try{
LogService.this.stop();
}catch(InterruptedException e){}
}
});
}
守护线程:
线程分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器以及其他执行辅助工作的线程)。当创建一个新线程时,新线程将继承创建它的线程的守护状态,因此在默认情况下,主线程创建的所有线程都是普通线程。
当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出。
我们应尽可能少地使用守护线程——很少有操作能够在不进行清理的情况下被安全地抛弃。特别是,如果在守护线程中执行可能包含I/O操作的任务,那么将是一种危险的行为。守护线程最好用于执行“内部”任务,例如周期性地从内存的缓存中移除逾期的数据。此外,守护线程通常不能用来替代应用程序管理程序中各个服务的生命周期。
终结器:
当不再需要内存资源时,可以通过垃圾回收器来回收它们。但对于其他一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显式地交还给操作系统。为了实现这个功能,垃圾回收器对那些定义了finalize方法的对象会进行特殊处理:在回收器释放它们后,调用它们的finalize方法,从而保证一些持久化的资源被释放。
由于终结器可以在某个可以由JVM管理的线程中运行,因此终结器访问的任何状态都可能被多个线程访问,这样就必须对其访问操作进行同步。终结器并不能保证它们将在何时运行甚至是否会运行,并且复杂的终结器通常还会在对象上产生巨大的性能开销。要编写正确的终结器是非常困难的。在大多数情况下,通过使用finally代码块和显式的close方法,能够比使用终结器更好地管理资源。唯一的例外情况在于:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。基于这些原因以及其他一些原因,我们要尽量避免编写或使用包含终结器的类(除非是平台库中的类)。避免使用终结器。
第八章:线程池的使用
8.1 在任务与执行策略之间的隐性耦合
有些类型的任务需要明确地指定执行策略:
- 依赖性任务。
- 使用线程封闭机制的任务。
- 对响应时间敏感的任务。
- 使用ThreadLocal的任务。
只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成“拥塞”。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则将可能造成死锁。
在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保它们依赖的任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。
/**
* ThreadDeadlock
* <p/>
* Task that deadlocks in a single-threaded Executor
*/
public class ThreadDeadlock {
ExecutorService exec = Executors.newSingleThreadExecutor();
public class LoadFileTask implements Callable<String> {
private final String fileName;
public LoadFileTask(String fileName) {
this.fileName = fileName;
}
public String call() throws Exception {
// Here's where we would actually read the file
return "";
}
}
public class RenderPageTask implements Callable<String> {
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// Will deadlock -- task waiting for result of subtask
return header.get() + page + footer.get();
}
private String renderBody() {
// Here's where we would actually render the page
return "";
}
}
}
每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。
有一项技术可以缓解执行时间较长的任务造成的影响,即限定任务等待资源的时间,而不要无限制地等待。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本。
8.2 设置线程池的大小
要想正确地设置线程池的大小,必须分析计算环境、资源预算和任务的特性。对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。
可以通过Runtime来获得CPU的数目:
int N_CPUS = Runtime.getRuntime().availableProcessors();
当然,CPU周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。
8.3 配置ThreadPoolExecutor
基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。在创建ThreadPoolExecutor初期,线程并不会立即启动,而是等到有任务提交时才会启动,除非调用prestartAllCoreThreads。
基本的任务排队方法有3种:无界队列、有界队列和同步移交(Synchronous Handoff)。
SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。否则根据饱和策略,这个任务将被拒绝。使用直接移交更高效。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。
对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择。它能提供比固定大小的线程池更好的排队性能,这种性能的差异是由于使用了SynchronousQueue(在Java 6中提供了一个新的非阻塞算法来替代SynchronousQueue,该算法把Executor基准的吞吐量提高了3倍)。
只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。
饱和策略:
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
创建一个固定大小的线程池,并采用有界队列及“调用者运行”饱和策略:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
N_THREADS, N_THREADS, 0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(CAPACITY)
);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。
在调用完ThreadPoolExecutor的构造函数后,仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数。如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。
ExecutorService exec = Executors.newCachedThreadPool();
if(exec instanceof ThreadPoolExecutor)
((ThreadPoolExecutor)exec).setCorePoolSize(10);
else
throw new AssertionError("Oops, bad assumption");
在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置。newSingleThreadExecutor返回按这种方式封装的ExecutorService,而不是最初的ThreadPoolExecutor。你可以在自己的Executor中使用这项技术以防止执行策略被修改。如果将ExecutorService暴露给不信任的代码,又不希望对其进行修改,就可以通过unconfigurableExecutorService来包装它。
ThreadPoolExecutor是可扩展的,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。如果任务完成后带有一个Error,那么就不会调用afterExecute。如果beforeExecute抛出一个RuntimeException,那么任务将不被执行,并且afterExecute也不会被调用。
/**
* TimingThreadPool
* <p/>
* Thread pool extended with logging and timing
*/
public class TimingThreadPool extends ThreadPoolExecutor {
public TimingThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("TimingThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
}
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns",
t, r, taskTime));
} finally {
super.afterExecute(r, t);
}
}
protected void terminated() {
try {
log.info(String.format("Terminated: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}
如果循环中的迭代操作都是独立的,并且不需要等待所有的迭代操作都完成再继续进行(并且每个迭代操作执行的工作量比管理一个新任务时带来的开销更多),那么就可以使用Executor将串行循环转化为并行循环。
void processInParallel(Executor exec, List<Element> elements){
for(final Element e : elements)
exec.execute(new Runnable(){
public void run(){ process(e); }
});
}
第九章:图形用户界面应用程序
9.1 为什么GUI是单线程的
在多线程的GUI框架中更容易发生死锁问题。
Swing的单线程规则是:Swing中的组件以及模型只能在这个事件分发线程中进行创建、修改以及查询。
与所有的规则相同,这个规则也存在一些例外情况。单线程规则的一些例外情况包括:
- SwingUtilities.isEventDispatchThread,用于判断当前线程是否是事件线程。
- SwingUtilities.invokeLater,该方法可以将一个Runnable任务调度到事件线程中执行(可以从任意线程中调用)。
- SwingUtilities.invokeAndWait,该方法可以将一个Runnable任务调度到事件线程中执行,并阻塞当前线程直到任务完成(只能从非GUI线程中调用)。
- 所有将重绘(Repaint)请求或重生效(Revalidation)请求插入队列的方法(可以从任意线程中调用)。
- 所有添加或移除监听器的方法(这些方法可以从任意线程中调用,但监听器本身一定要在事件线程中调用)。
/**
* SwingUtilities
* <p/>
* Implementing SwingUtilities using an Executor
*/
public class SwingUtilities {
private static final ExecutorService exec =
Executors.newSingleThreadExecutor(new SwingThreadFactory());
private static volatile Thread swingThread;
private static class SwingThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
swingThread = new Thread(r);
return swingThread;
}
}
public static boolean isEventDispatchThread() {
return Thread.currentThread() == swingThread;
}
public static void invokeLater(Runnable task) {
exec.execute(task);
}
public static void invokeAndWait(Runnable task)
throws InterruptedException, InvocationTargetException {
Future f = exec.submit(task);
try {
f.get();
} catch (ExecutionException e) {
throw new InvocationTargetException(e);
}
}
}
可以将Swing的事件线程视为一个单线程的Executor,它处理来自事件队列的任务。
/**
* GuiExecutor
* <p/>
* Executor built atop SwingUtilities
*/
public class GuiExecutor extends AbstractExecutorService {
// Singletons have a private constructor and a public factory
private static final GuiExecutor instance = new GuiExecutor();
private GuiExecutor() {
}
public static GuiExecutor instance() {
return instance;
}
public void execute(Runnable r) {
if (SwingUtilities.isEventDispatchThread())
r.run();
else
SwingUtilities.invokeLater(r);
}
public void shutdown() {
throw new UnsupportedOperationException();
}
public List<Runnable> shutdownNow() {
throw new UnsupportedOperationException();
}
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
throw new UnsupportedOperationException();
}
public boolean isShutdown() {
return false;
}
public boolean isTerminated() {
return false;
}
}
9.2 短时间的GUI任务
为了简便,短时间的任务可以把整个操作都放在事件线程中执行,而对于长时间的任务,则应该将某些操作放到另一个线程中执行。
Swing将大多数可视化组件都分为两个对象,即模型对象与视图对象。在模型对象中保存的是将被显示的数据,而在视图对象中则保存了控制显示方式的规则。
9.3 长时间的GUI任务
通过Future来表示一个长时间的任务,可以极大地简化取消操作的实现。在FutureTask中也有一个done方法同样有助于实现完成通知。
/**
* BackgroundTask
* <p/>
* Background task class supporting cancellation, completion notification, and progress notification
*/
public abstract class BackgroundTask <V> implements Runnable, Future<V> {
private final FutureTask<V> computation = new Computation();
private class Computation extends FutureTask<V> {
public Computation() {
super(new Callable<V>() {
public V call() throws Exception {
return BackgroundTask.this.compute();
}
});
}
protected final void done() {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
V value = null;
Throwable thrown = null;
boolean cancelled = false;
try {
value = get();
} catch (ExecutionException e) {
thrown = e.getCause();
} catch (CancellationException e) {
cancelled = true;
} catch (InterruptedException consumed) {
} finally {
onCompletion(value, thrown, cancelled);
}
};
});
}
}
protected void setProgress(final int current, final int max) {
GuiExecutor.instance().execute(new Runnable() {
public void run() {
onProgress(current, max);
}
});
}
// Called in the background thread
protected abstract V compute() throws Exception;
// Called in the event thread
protected void onCompletion(V result, Throwable exception,
boolean cancelled) {
}
protected void onProgress(int current, int max) {
}
// Other Future methods just forwarded to computation
public boolean cancel(boolean mayInterruptIfRunning) {
return computation.cancel(mayInterruptIfRunning);
}
public V get() throws InterruptedException, ExecutionException {
return computation.get();
}
public V get(long timeout, TimeUnit unit)
throws InterruptedException,
ExecutionException,
TimeoutException {
return computation.get(timeout, unit);
}
public boolean isCancelled() {
return computation.isCancelled();
}
public boolean isDone() {
return computation.isDone();
}
public void run() {
computation.run();
}
}
9.4 共享数据模型
只要阻塞操作不会过度地影响响应性,那么多个线程操作同一份数据的问题都可以通过线程安全的数据模型来解决。
如果在程序中既包含用于表示的数据模型,又包含应用程序特定的数据模型,那么这种应用程序就被称为拥有一种分解模型设计。如果一个数据模型必须被多个线程共享,而且由于阻塞、一致性或复杂度等原因而无法实现一个线程安全的模型时,可以考虑使用分解模型设计。
9.5 其他形式的单线程子系统
线程封闭不仅仅可以在GUI中使用,每当某个工具需要被实现为单线程子系统时,都可以使用这项技术。有时候,当程序员无法避免同步或死锁等问题时,也将不得不使用线程封闭。
第三部分:活跃性、性能与测试
本文到此处告一段落…
P183