《Effective Java》第10章 发并

Posted ITRoad

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Effective Java》第10章 发并相关的知识,希望对你有一定的参考价值。

第66条:同步访问共享的可变数据

Java语言规范保证读或者写一个变量是原子的(atomic ) ,除非这个变量的类型为long或者double.
[java中long和double类型操作的非原子性探究](
http://blog.csdn.net/zhaifengmin/article/details/46315003)


你可能期待这个程序运行大约一秒钟左右,之后主线程将stapRequested设置为true,致使后台线程的循环终止。但是在我的机器上,这个程序永远不会终止:因为后台线程永远在循环!

活性失败
问题在于,由于没有同步,就不能保证后台线程何时“看到”主线程对stopRequested的值所做的改变。没有同步,虚拟机将这个代码:

  1. while(!done){
  2. i++;
  3. }

转变为:

  1. if(!done){
  2. while(true){
  3. i++;
  4. }
  5. }

这是可以接受的。这种优化称作提升(hoisting ),正是HotSpot Server VM的工作。结果是个活性失败(likeness failure):这个程序无法前进。

第一种修正:同步读写
修正这个问题的一种方式是同步访问stopRequested域。这个程序会如预期般在大约一秒钟之内终止:

注意写方法(requestStop)和读方法(stopRequested)都被同步了。只同步写方法还不够! 实际上,如果读和写操作没有都被同步,同步就不会起作用

使用volatile修饰符
如果stopRequested被声明为volatile,第二种版本的StopThread中的锁就可以省略。虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值:

安全性失败
在使用volatile的时候务必要小心。考虑下面的方法,假设它要产生序列号:

问题在于,增量操作符(++)不是原子的。它在nextSerialNumber域中执行两项操作:首先它读取值,然后写回一个新值。相当于原来的值再加上1。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败( safety failure):这个程序会计算出错误的结果。

修复一:使用synchronised修饰方法
修正generateSerialNumber方法的一种方法是在它的声明中增加synchronised修饰符。这样可以确保多个调用不会交叉存取,确保每个调用都会看到之前所有调用的效果。一旦这么做,就可以且应该从nextSerialNumber中删除volatile修饰符。

修复一:使用AtomicLong
使用类AtomicLong,它所做的工作正是你想要的,并且有可能比同步版GenerateSerialNumber执行得更好:

简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。

第67条:避免过度同步

为了避兔活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃时客户端的按制口换句话说,在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。

通常,你应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个很耗时的动作,则应该设法把这个动作移到同步区域的外面,而不违背第66条中的指导方针.

在这个多核的时代,过度同步的实际成本并不是指获取锁所花费的CPU时间;而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力。

如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁(lock splitting )、分离锁(lack striping)和非阻塞(nonblocking)井发控制。这些方法都超出了本书讨论范围。

第68条:executor和task优先于线程

如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool通常是个不错的选择,因为它不需要配置,并且一般情况下能够正确地完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了! 在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPonlExecutor类。

你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在关键的抽象不再是Thread了,它以前可是既充当工作单元,又是执行机制。现在工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务(task)。任务有两种:Runnable及其近亲Callable(它与Runnable类似,但它会返回值)。执行任务的通用机制是executor services.

第69条:并发工具优先于wait和notify

正确地使用wait和notify伪比较困难,就应该用更高级的并发工具来代替。

java.util.concurrent中更高级的工具分成三类:Executor Framework(已介绍)、并发集合(Concurrent Collection)以及同步器(Synchronizer).

并发集合
并发集合为标准的集合接口(如List, Queue和Map)提供了高性能的并发实现。为了提供高并发性,这些实现在内部自己管理同步(见第67条)。因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢。

优先使用ConcurrentNashMap,而不是使用Colfections.synchronizedMap或者Hashtable。只要用并发Map替换老式的同步Map,就可以极大地提升并发应用程序的性能。更一般地,应该优先使用并发集合,而不是使用外部同步的集合。

有些集合接口已经通过阻塞操作(blocking operation)进行了扩展,它们会一直等待(或者阻塞)到可以成功执行为止。例如,BlockingQueue扩展了Queue接口,并添加了包括take在内的几个方法,它从队列中删除并返回了头元素,如果队列为空,就等待。这样就允许将阻塞队列用于工作队列(work queue),也称作生产者一消费者队列(producer-consumer
queue),一个或者多个生产者线程(producer thread)在工作队列中添加工作项目,并且当工作项日可用时,一个或者多个消费者线程(consumer thread )贝al从工作队列中取出队列并处理工作项目。不出所料。人多数ExecutarService实现(包括ThreadPoolExecutor)都使用BlockingQueue。

同步器
同步器(Synchronizer)是一些使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch和Semaphore. 较不常用的是CyclicBarrier和Exchanger.

对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMills. System.nanoTime更加准确也更加精确,它不受系统的实时时钟的调整所影响。

第70条:线程安全性的文档化

一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。下面的列表概括了线程安全性的几种级别。这份列表并没有涵盖所有的可能,而只是些常见的情形:

  • 不可变的(immutable)
    这个类的实例是不变的。所以,不需要外部的同步。这样的例子包括String, Long和BigInteger。
  • 无条件的线程安全(unconditionally thread-safe)
    这个类的实例是可变的,但是这个类有着足够的内部同步,所以,它的实例可以被并发使用,无需任何外部同步。其例子包括Random和ConcurrentJashMap.
  • 有条件的线程安全(conditionally thread-safe)
    除了有些方法为进行安全的并发使用而需要外部同步之外,这种线程安全级别与无条件的线程安全相同。这样的例子包括Cnllections.synchronized包装返回的集合,它们的迭代器(iteratar)要求外部同步。
  • 非线程安全(not thread-safe)
    这个类的实例是可变的。为了并发地使用它们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)。这样的例子包括通用的集合实现,例如lArrayList和HashMap。

第71条:慎用延迟初始化

如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder
class模式。
这种模式(也称initialize-on-demand holder class idiom)保证了类要到被用到的时候才会被初始化。如下所示:

当getField方法第一次被调用时,它第一次读取FieldHolder.field,导致FieIdHalder类得到初始化。这种模式的魅力在于,getField方法没有被同步,并且只执行一个域访问,因此延迟初始化实际上井没有增加任何访问成本。

如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式(double-check idiom )。这种模式避免了在域被初始化之后访问这个域时的锁定开销。这种模式背后的思想是:两次检查域的值 [因此名字叫双重检查(double-check) ], 第一次检查时没有锁定,看看这个域是否被初始化了; 第二次检查时有锁定。只有当第二次检查时表明这个域没有被初始化,才会调用computeFieidValue方法对这个域进行初始化。因为如果域已经被初始化就不会有锁定,域被声明为volatile很重要(见第66条)。下面就是这种习惯樟式:

这段代码可能看起来似乎有些费解。尤其对于需要用到局部变量result可能有点不解。这个变量的作用是确保field只在已经被初始化的情况下读取一次,提高性能。

以上是关于《Effective Java》第10章 发并的主要内容,如果未能解决你的问题,请参考以下文章

《Effective Java》第5章 泛型

[读书笔记]《Effective Java》第9章异常

《Effective Java》第7章 方法

《Effective Java》第9章 异常

《Effective Java》第6章 枚举和注解

《Effective Java 2nd》第7章 方法