Effective Java, 2nd - Concurrency - Notes

来源:互联网 发布:curl json 格式化 编辑:程序博客网 时间:2024/06/01 10:42

并发 (Concurrency)


Item 66: 同步访问共享可变数据(Synchronize access to shared mutable data )

Synchronized关键字确保了在同一时间只有一个线程能执行或阻塞一个方法。

不仅防止一个对象被以不一致的状态观察到,而且确保每个进入或阻塞方法的线程能看到所有之前的修改带来的影响。

 语言规范确保了,读写一个long或double之外的变量,都是原子操作,即可确保读到的值是存入的值,即便是多个线程并行修改这个变量而且未进行同步。

 有人说为了提升性能,应该避免对原子性数据的读写进行同步。这种说法极其错误。原子性并不保证一个线程的修改对其他线程可见。

 线程间的可信通信,如互斥一样也要求同步。

 同步失败的后果是可怕的。考虑,从一个线程停止另一个线程,不要使用Tread.stop, (已经被弃用),不安全,会导致数据破坏。使用标识线程停止的成员变量,需要同步访问。

 读和写操作都需要进行同步,否则同步是失效的。这里的同步用于通信,而非互斥。

考虑下面的方法,线程可能永远不会停止。

import java.util.concurrent.TimeUnit;public class StopThread {//use volatile to make sure Thread read stopRequested immediatelyprivate static /*volatile*/ boolean stopRequested; public static void main(String[] args)throws InterruptedException {Thread backgroundThread = new Thread(new Runnable() {public void run() {int i = 0;while (!stopRequested) {i++;}/* VM may transform into this codeif (!done)while (true)i++;*/}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);stopRequested = true;}}


下面是正确的stop线程方法。 另一种方法,声明为volatile能保证每个线程都读到最近的值。

import java.util.concurrent.TimeUnit;public class StopThreadOK {private static boolean stopRequested;private static synchronized void requestStop() {stopRequested = true;}private static synchronized boolean stopRequested() {return stopRequested;}public static void main(String[] args)throws InterruptedException {Thread backgroundThread = new Thread(new Runnable() {public void run() {int i = 0;while (!stopRequested())i++;}});backgroundThread.start();TimeUnit.SECONDS.sleep(1);requestStop();}}


 a++; 不是原子操作。

 避免一些同步问题的策略,可以将可变数据限制在一个单独的线程,只暴露不可变数据。

共享对象的引用,其他线程不需要同步就可以看到对象的改变,这种对象叫高效不变量(effective immutable),这种方式叫安全暴露(safepublication)

 线程间通信(非互斥访问),可以使用volatile来同步,但正确使用需要技巧。

 

Item 67: 避免过度同步(Avoid excessive synchronization )

过度同步可能导致,性能下降、死锁、不确定的行为。

 为避免活动性(死锁)和安全性的失败,永远不要在一个被同步的方法或区域中将控制交给客户端。换句话说,在一个同步的域,不要调用一个被设计用于重载(override)的方法,或客户端以函数对象(function object)形式提供的方法(回调)。这些外来方法(alien method)的行为不能预测和控制,可能导致异常、死锁、或数据破坏。

 implement an observableset wrapper.

 Java编程语言中的锁(locks)是可重入的(reentrant)。在同一个线程中,通过synchronized获得锁后,在synchronized域(方法)中,可以再度获得锁。

synchronized(object){

// synchronizedregion

synchronized(object) {

  //重入 (此处修改object可导致java.util.ConcurrentModificationException)

}

}

本质上,这是锁的失效。可重入锁简化了面向对象的多线程编程,但将活动性失败转变成了安全性失败(即将死锁变成了异常)。

 如果上面的object是ArrayList,CopyOnWriteArrayList是设计用来解决此类问题的。

 作为一条规则,你应该在同步域(synchronized regions)内部做尽可能少的事情。


 关于性能。

过度同步的真正开销不在于获得锁的CPU时间,而在于失去并行处理的机会和需要保持内存数据观察一致性而导致的延迟。另一个潜在的开销在于,限制了VM优化代码的能力。

 如果一个可变类目的用于并发使用,你应该使之线程安全,而且在对象内部同步比锁住整个对象,能获得更高的并发性。

除此之外,不要在内部同步。让客户端在适当的地方做外部同步。早期,Java平台很多类违反了此规范。

如果不确定,不要同步你的类,而通过文档指明它不是线程安全的。

 

如果确实要在类内部同步,有些技术可以达到高并发性,如lock splitting, lock striping, and nonblocking concurrency control.

 

修改静态域的方法,必须同步对静态域的访问。客户端不太可能做外部同步,因为一个客户端不能确保其他客户端也做类似的同步。

Item 68: 优先使用executors和tasks(Prefer executors and tasks to threads)

写一个工作队列(work queue),这个类允许客户端们将工作项入队,通过后台线程进行异步处理。当不再需要工作队列的时候,客户端可以调用一个方法,让后台线程在处理完所有工作后终止。

上面这个实现很简单,但是也要写一些专门的代码,稍不注意就容易出错。在Java1.5中,加入了java.util.concurrent包,其中包含的Executor Framwork就是一个灵活的基于接口的任务执行工具。只需一行代码:

ExecutorService executor = Executors.newSingleThreadExecutor();

提交可执行任务:

executor.execute(runnable);

终止执行器:

executor.shutdown();

 使用ExecutorService,你可以做更多的事情。例如,你可以等待一个特定的任务结束,等待任意或所有的任务集合结束(使用invokeAny或invokeAll),等待executor的终止(使用awaitTermination),你可以依次取得任务完成后的结果(使用ExecutorCompletionService),等等。

 如果你想要队列不只一个线程处理请求,只要简单地创建名为Thread Pool的ExecutorService, 创建固定或可变数目的线程池。直接使用ThreadPoolExecutor,可以控制thread pool的几乎所有操作。

 如何选择executor service有技巧。一个小程序或轻负载服务,使用Executors.newCachedThreadPool是一个好的选择,因为它不需要配置而且能完成任务。但对于重负载的产品型服务,它不是一个好的选择!在CachedThreadPool中,提交的任务不进队列,而是直接交给一个线程处理,如果没有线程可用,将创建一个新的线程。在重负载情况下,新任务到来时更多新线程被创建,将使负载情况变得更糟。这种情况下最好使用Executors.newFixedThreadPool.

 Executor Framework不仅使你不用再写工作队列,而且不用再直接与Thread打交道。Thread做为工作(work)和执行机制(execution)的双重单元,而现在工作和执行机制被分离开了。关键抽象在于工作单元,称为任务(task)。有两种任务:Runnable及关闭,Callable(类似Runnable,除了返回一个值)。本质上,Executor Framework对execution作用,就如同Collections Framework之于聚合的作用。

 ScheduledThreadPoolExecutor可作为java.util.Timer的替代。Timer使用更简单,而scheduledthread pool executor更加灵活。Timer只使用一个线程,长时任务可能影响时间精度,如果跑出异常将退出执行。而scheduled thread pool executor支持多线程,而且能从异常恢复。

 

Item 69: 优先使用并发工具类而不是wait和notify(Prefer concurrency utilities to wait and notify)

考虑到正确使用wait和notify的难度,你应该以使用更高层的Concurrency utilities来代替。

 java.util.concurrent中的工具归为3类:Executor Framework, concurrent collections, 和synchronizers.此条款中涵盖了Concurrentcollections 和synchronizers.

 concurrentcollections 提供了标准集合接口,如list, Queue, Map, 的高性能并发实现。这些在内部管理同步来实现高并发,因此无法排除一个并发集合的并发行为;对其加锁将不起作用,而只是减慢程序。

 除了提供了优秀的同步,ConcurrentHashMap非常快。除非有充分的理由,优先使用ConcurrentHashMap,而不是Collections.synchronizedMap或 Hashtable.

 一些接口扩展了阻塞操作(blocking operations),等待成功执行。BlockingQueue增加了几个方法,包括take,移除并返回头元素,如果队列为空则等待。这使得阻塞队列可以作为工作队列(也称为生产者-消费者队列, producer-consumer queues),一个或多个生产者线程放入任务,一个或多个消费者线程取出任务并处理。如你所想,大部分ExecutorService,包括ThreadPoolExecutor,使用BlockingQueue.

 同步器(Synchronizers)是这样一些对象,能使线程相互等待,协同工作。最常用的是CountDownLatch和Semaphore,不常用的是CyclicBarrier和Exchanger.(原语)

 CountDownLatch是单次使用的阻栏,允许一个或多个线程等待一个或多个其他线程做些工作。唯一的构造函数带有一个int参数,表示允许等待线程执行前,countDown()必须被调用的次数。

 对应间隔计时,总是优先使用System.nanoTime而不是System.currentTimeMillis, 更准确和精确,并不被系统时间影响。

// 使用wait方法的标准写法

// The standard idiom for using the waitmethod

synchronized (obj) {

while (<condition does not hold>)

obj.wait(); // (Releases lock, andreacquires on wakeup)

... // Perform action appropriate to condition

}

 总是使用wait loop写法来调用wait方法;永远不要在循环外调用它。

 当condition不持有,一个线程可能被唤醒的几个理由:

在线程调用notify和等待线程唤醒的时间之间,另一个线程可能获得锁并改变状态。

另一个线程可能无意或有意调用了notify。

Notify线程可能过度唤醒等待线程。例如,即便只要部分线程条件满足,也调用notifyAll.

可能在没有notify的情况下唤醒(称为伪唤醒)。

 使用wait和notify就像使用“并发汇编语言”,相对于java.util.concurrent提供高层语言来说。这不是一在新的代码中使用wait和notify的理由。

 

Item 70: 用文档说明线程安全(Document thread safety)

一个类的对象和静态方法在并发使用时会如何,是类跟客户端之间的一个重要约定。

 为了安全的并发使用,一个类必须清楚的用文档说明它支持哪一个级别的线程安全。

下面列出了几个概括的线程安全级别:

immutable– 类的对象是常量,不需要外部同步。如String, Long, 和BigInteger.

unconditionallythread-safe – 无条件的线程安全,类的对象可变,但是有足够的内部同步,不需要同步而可以并发使用。如Random和ConcurrentHashMap.

conditionallythread-safe – 有条件的线程安全。

notthread-safe – 类的对象时可变的。需要同步使用。如ArrayList和HashMap.

thread-hostile– 即便进行外部了同步也不能安全地并发使用。

 

使用私有成员对象锁,保护同步不被客户代码和子类干预。

// Private lock object idiom - thwartsdenial-of-service attack

private final Object lock = new Object();


Item 71: 谨慎使用延迟初始化(Use lazy initialization judiciously)

Lazy initialization是一把双刃剑,最好的建议是“非必要不使用”。

 多线程情况下,延迟初始化是复杂的。如果两个或更多线程共享一个延迟初始化变量,同步将产生风险,或导致bugs.

 大多数情况下,普通的初始化都优于延迟初始化。 

// Normal initialization of an instancefield

private final FieldType field =computeFieldValue();

 

// 延迟初始化,使用同步访问。

// Lazy initialization of instance field -synchronized accessor

private FieldType field;

synchronized FieldType getField() {

if (field == null)

field = computeFieldValue();

return field;

}

 

// 在静态域上使用延迟初始化

// Lazy initialization holder class idiomfor static fields

private static class FieldHolder {

static finalFieldType field = computeFieldValue();

}

static FieldType getField() { returnFieldHolder.field; }

 

// 在对象域上使用延迟初始化

避免了变量初始化完成后,每次访问变量时的锁开销。因为变量已初始化时没有加锁,它申明为volatile很关键。

// Double-check idiom for lazyinitialization of instance fields

private volatile FieldType field;

FieldType getField() {

FieldType result = field;

if (result == null) { //First check (no locking)

synchronized(this) {

result = field;

if (result == null) // Secondcheck (with locking)

field = result = computeFieldValue();

}

}

return result;

}

 

// 可以容忍重复初始化

// Single-checkidiom - can cause repeated initialization!

private volatileFieldType field;

private FieldType getField() {

FieldType result = field;

if (result == null)

field = result = computeFieldValue();

return result;

}

 

Item 72: 不要依赖线程调度器(Don’t depend on the thread scheduler)

当有很多可执行线程时,线程调度器(操作系统)会决定哪部分先执行,执行多久。任何合理的操作系统会让这个决策公平,但策略可能不尽相同。因此,好的程序不应该依赖这些具体策略。任何依赖线程调度器来保证正确性和性能的程序,很可能是不可移植的。

 

写健壮、灵敏、可移植程序的最好方法是确保平均的可执行线程数(非等待线程),不显著地大于处理器的数目。这确保调度的结果差异不大。

 如果不做有用的工作,线程不应该运行。即线程池大小合适,任务适当小并且相互独立。但任务过分小,会分发开销会影响性能。

 线程不应该忙等(busy-wait),不能反复检查一个共享对象等待某些工作完成。

 Thread.yield没有可验证的语义,不同JVM实现上可能有完全不同的效果。应该使用Thread.sleep(1) 代替Thread.yield来做并发测试。

 线程优先级是Java平台上最不可移植的特性之一。

 

Item 73: 避免用线程组(Avoid thread groups)

thread groups 最初是为隔离程序的安全目的设计的机制,但从未实现目标。部分接口被弃用。你可以用thread pool executor代替。

 

 

 

 

 


0 0