66:同步访问共享的可变数据
synchronized:1互斥,阻止线程看到的对象处于不一致的状态;2保证线程在进入同步区时能看到变量的被各个线程的所有修改
Java中,除了long或者double,“读”或者“写”一个变量是原子的。注意:是读或者写单个动作是源自的,而不是读写这两个动作整体是原子的。
由于虚拟机会对代码进行优化,所以可能会导致一些错误:可能你想的是在另一线程中改变done的值来终止while循环,但是优化之后却无法做到这样。要避免这样的优化错误,就必须对done同步。
//优化前,即程序员所写 while(!done){ ++i; } //优化后 测试环境为win7的Eclipse不会进行此优化,在HopSpot Server VM中会进行此优化 if(!done){ wile(true){ ++i; } }
volatile:可以保证线程之间的通信效果,但是无法保证互斥访问。即任何读线程读到的最近一次由任何线程修改后的值。但应该特别注意一些操作的原子性,比如i++,++操作会先读后写,即即使有volatile,也可能会在++操作读了值之后,+1操作之前而读取到值,即没有读到+1后的值。
67:避免过度同步
避免过度同步:为了避免死锁和数据破坏(一般由锁的可重入机制造成),千万不要在同步区域内部调用外来方法(即可能被覆盖的方法或者由客户端以函数对象提供的方法),应该尽量限制同步区域内部的工作量。外来方法的调用应放在同步区域之外,这叫做“开放调用”,可以避免死锁并极大提高并发性。
当设计一个类的时候,需要考虑是否应该在类的内部实现同步,使之线程安全,并能获得更高的并发性。在这个多核时代,这比不要过度同步更为重要。
过度同步的坏处并不是指获取锁所花费的CPU时间,而是:1失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟;2会限制VM优化代码执行的能力
68:executor和task优先于线程
即优先使用Executor和task(即Runnable和Callable)而不是Thread,可以降低创建的线程个数,提高性能,而且可以获得更多的线程策略,极大降低编码难度。但应该认真选择合适的ExecutorService,比如一般的轻量程序选择Executors.newCachedThreadPool,但是对于高负载的服务器,由于缓存线程池可能会根据需求而不断增加新线程,可能导致CPU全部被占用,最终导致奔溃,这时就应该选择Executor.newFixedThreadPool以限制总线程数。
69:并发工具优于wait和notify
优先使用java.util.concurrent中的并发工具,因为使用notify-wait可能存在以下情况使等待线程在条件不满足的情况下苏醒过来:
- 另一个线程可能已经得到了锁,并且从一个线程调用notify的那一刻起,到等待线程苏醒过来的这段时间,得到锁的线程已经改变了受保护的状态
- 其他线程意外或者恶意调用notify
- 通知线程过度大方,使用notifyAll唤醒了某些并不满足条件的线程
- 在没有通知的情况下,等待线程也可能苏醒过来(很少)
如果需要使用notify-wait模式,则:
- 始终使用wait循环模式调用wait,而不应该在循环之外调用wait
- 从优化的角度看,如果所有的等待线程都在等待同一个条件,而每次只有一个线程可以从这个条件唤醒,那么应该使用notify。
- 优先使用notifyAll而不是notify,以保证程序的活性,虽然使用notifyAll会唤醒一些不相关线程,造成一定的性能问题,但是这会保证程序的正确性,而且可以避免恶意程序吞掉了某个notify。
70:线程安全性的文档化
每个类都应该有对线程安全的文档说明。线程安全分为五个级别:
- 不可变的:例如String,Long,BigInteger,不需要外部同步
- 无条件的线程安全:类内部已进行足够的同步,无需外部的同步。应该考虑使用使用私有锁对象来代替同步的方法,以防止客户端程序和子类的不同步干扰。
- 有条件的线程安全:部分方法需要外部同步
- 非线程安全:客户必须对每个调用都进行同步,例如通用集合,如ArrayList,HashMap
- 线程对立的:即使有外部同步,也不能安全的被并发使用
71:慎用延迟初始化
大部分情况应该正常的初始化,除非域只有在类的实例部分被访问,并且这个域的初始化成本很高,则可能值得延迟初始化。
1 //静态域应该使用lazy initialization holder class模式 2 private static class FieldHolder{ 3 static final FieldType field = computeFieldValue(): 4 } 5 6 static FieldType getField() {return FieldHolder.field;} //当它第一次被调用时,第一次读取field,导致FieldHolder类得到初始化 7 8 9 //对于实例域应该使用双重检查模式 10 private volatile FieldType field; 11 FieldType getField(){ 12 FieldType result = filed; //result的作用:在field被初始化的情况下只读取field一次 13 if(result == null) { //假如没有result,读取field 14 synchronized(this) { 15 result = field; 16 if(result == null) { //可能在获得锁的期间,field被其他线程初始化了 17 field = result = computeFieldValue(): 18 } 19 } 20 return result; //假如没有result,读取field 21 } 22 23 //对于可以接受重复初始化的字段,即computeFieldValue():消耗不大的字段,则可以省去第二次检查,变成单重检查模式 24 private volatile FieldType field; 25 FieldType getField(){ 26 FieldType result = filed; 27 if(result == null) { 28 field = result = computeFieldValue(): 29 } 30 return result; 31 }
72:不要依赖于线程调度器
由于调度算法的区别,任何依赖线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。也不应该依赖Thread.yield(对于一般程序员来说,其唯一用途是在测试期间人为的增加程序并发性)以及线程优先级(线程优先级是Java平台上最不可移植的特性),线程优先级可以提高一个已经能正常工作的程序的服务质量,但永远不应该用来修正一个原本不能正常工作的程序
要编写健壮的、响应良好的、可移植的多线程程序,最好的办法是保证可运行的线程平均数量不明显多于处理器数量。而保证可运行线程数量尽可能少的方法是让每个线程做些有意义的工作。
73:避免使用线程组
线程组(ThreadGroup)存在很多缺陷,已经过时。如果需要处理线程的逻辑组,应该使用Executor