《Effective Java》第10章 发并
第66条:同步访问共享的可变数据
Java语言规范保证读或者写一个变量是原子的(atomic ) ,除非这个变量的类型为long或者double.
[java中long和double类型操作的非原子性探究](
http://blog.csdn.net/zhaifengmin/article/details/46315003)
你可能期待这个程序运行大约一秒钟左右,之后主线程将stapRequested设置为true,致使后台线程的循环终止。但是在我的机器上,这个程序永远不会终止:因为后台线程永远在循环!
活性失败
问题在于,由于没有同步,就不能保证后台线程何时“看到”主线程对stopRequested的值所做的改变。没有同步,虚拟机将这个代码:
while(!done){
i++;
}
转变为:
if(!done){
while(true){
i++;
}
}
这是可以接受的。这种优化称作提升(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 第十一章 系列化
EFFECTIVE JAVA 第十一章 系列化(将一个对象编码成一个字节流) 74.谨慎地实现Serializable接口 *实现Serializable接口付出的代价就是大大降低了“改变这个类 ...
- effective java 第2章-创建和销毁对象 读书笔记
背景 去年就把这本javaer必读书--effective java中文版第二版 读完了,第一遍感觉比较肤浅,今年打算开始第二遍,顺便做一下笔记,后续会持续更新. 1.考虑用静态工厂方法替代构造器 优 ...
- [Effective Java]第四章 类和接口
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- [Effective Java]第三章 对所有对象都通用的方法
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 对于所有对象都通用方法的解读(Effective Java 第三章)
这篇博文主要介绍覆盖Object中的方法要注意的事项以及Comparable.compareTo()方法. 一.谨慎覆盖equals()方法 其实平时很少要用到覆盖equals方法的情况,没有什么特殊 ...
- [Effective Java]第六章 枚举和注解
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- [Effective Java]第七章 方法
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- [Effective Java]第五章 泛型
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 【Effective Java】10、java注解使用
package cn.xf.cp.ch02.item35; import java.lang.annotation.ElementType; import java.lang.annotation.R ...
随机推荐
- MySQL_截止昨日南京市所有在职业务员业绩排名-20170116
#计算南京销售员总业绩排名 数据结果已打乱处理 #职工信息表包含在职和离职两种状态 因此不能以这表当做主表 不然离职人的数据也会出现 以毛利表为主表 销售员限制在昨天在职的销售范围内 且和后面left ...
- Webpack之“多页面开发”最佳实战
前言:相信之前看过这篇文章,前端构建工具之“Webpack”的朋友,对于Webpack有了一定的了解.那么今天就跟大家分享下:如何利用webpack,来进行多页面项目实战开发. 一.项目初始化安装 1 ...
- 类和对象(9)—— new和delete
对象动态建立和释放 new 和delete 在软件开发过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除.在C语言中是利用库函数malloc和free来分配和撤销内存空间的.C ...
- Oracle存储过程创建及调用
在大型数据库系统中,有两个很重要作用的功能,那就是存储过程和触发器.在数据库系统中无论是存储过程还是触发器,都是通过SQL 语句和控制流程语句的集合来完成的.相对来说,数据库系统中的触发器也是一种存储 ...
- Mybatis相关SQL操作总结
1.resultMap和resultType等参数以及结果集 <select id="getApplicationByRoleCode" resultType="p ...
- ubuntu 迁移部分 / 目录下的存储空间到 /home目录
状况:当时给系统分区的时候,home和根目录都是25GB左右,突然发现home 目录不够用了,于是决定进行将根目录的部分空间挪移到home下去 主要方法:使用Gparted的LIve USB的方法. ...
- mysql5.6之 传输表空间迁移表或恢复误删除的表
一,简单说明: 1),传输表空间的限制: 1,mysql 版本 5.6.6 及其以上,并且版本建议源和目标版本建议都是GA版并且大版本一样 2,表引擎为innodb并且开启独立表空间 innod ...
- think python chapter2
1.An assignment statement creates a new variable and gives it a value: Variable names can be as long ...
- [C++] 贪心算法之活动安排、背包问题
一.贪心算法的基本思想 在求解过程中,依据某种贪心标准,从问题的初始状态出发,直接去求每一步的最优解,通过若干次的贪心选择,最终得出整个问题的最优解. 从贪心算法的定义可以看出,贪心算法不是从整体上考 ...
- Java基础--虚拟机JVM
JVM内存结构 Heap Space: 堆内存(Heap Space)是由Young Generation和Old Generation组成,而Young Generation又被分成三部分,Eden ...