【转】effective java 笔记5

from:http://blog.csdn.net/ilibaba/archive/2009/06/01/4234248.aspx

NO.48 对共享可变数据的同步访问
同步,不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证通过一系列看似顺序执行的状态转变序列,对象从一种一致的状态变迁到另一种一致的状态。
synchronized关键字可以保证在同一时刻,只有一个线程在执行一条语句,或者一段代码块。java语言保证读或写一个变量是原子的,除非这个变量的类型是long或double.
java的内存模型决定,为了在线程之间可靠地通信,以及为了互斥访问,对原子数据的读写进行同步是需要的。看一个可怕的例子:

  1. //Broken - require synchronization!
  2. private static int nextSerialNumber=0; 
  3. public static int generateSerialNumber(){ 
  4. return nextSerialNumber++; 

对其改进,只需要在generateSerialNumber()的声明中增加synchronized修饰符即可。
为了终止一个线程,一种推荐的做法是让线程轮询某个域,该域的值如果发生变化,就表明此线程就应该终止自己。下面的例子就是这个思路,但在同步出了问题。

  1. //Broken - requires synchronization
  2. public class StoppableThread extends Thread{ 
  3. private boolean stopRequested=false; 
  4. public void run(){ 
  5. boolean done=false; 
  6. while(!stopRequested && !done){ 
  7.      ...//do what needs to be done in the thread
  8.    } 
  9. public void requestStop(){ 
  10.    stopRequested=true; 
  11.   } 

对其改进如下:

  1. //Properly synchronized cooperative thread temination
  2. public class StoppableThread extends Thread{ 
  3. private boolean stopRequested=false; 
  4. public void run(){ 
  5. boolean done=false; 
  6. while(!stopRequested() && !done){ 
  7.    ...//do what needs to be done in the thread
  8.   } 
  9. public synchronized void requestStop(){ 
  10.   stopRequested=true; 
  11. private synchronized boolean stopRequested(){ 
  12. return stopRequested; 

另一种改进是,将stopRequested声明为volatile,则同步可以省略。

关于Singleton:
再来看迟缓初始化(lazy initialization)问题,双重访问模式并不一定都能正常工作,除非被共享的变量包含一个原语值。看例子:

  1. //The double-check idion fro lazy initialization - broken!
  2. private static Foo foo=null; 
  3. public static Foo getFoo(){ 
  4. if (foo==null){ 
  5. synchronized(Foo.class){ 
  6. if(foo==null)foo=new Foo(); 
  7.     } 
  8.   } 
  9. return foo; 

最容易的修改是省去迟缓初始化:

  1. //normal static initialization (not lazy)
  2. private static finall Foo foo=new Foo(); 
  3. public static Foo getFoo(){ 
  4. return foo; 

或者使用正确的同步方法,但可能增加少许的同步开销:

  1. //properly synchronized lazy initialization
  2. private static Foo foo=null; 
  3. public static synchronized Foo getFoo(){ 
  4. if(foo==null)foo=new Foo(); 
  5. return foo; 

按需初始化容器模式也不错,但是它只能用于静态域,不能用于实例域。

  1. //The initialize-on-demand holder class idiom
  2. private static class FooHolder(){ 
  3. static final Foo foo=new Foo(); 
  4. public static Foo getFoo(){ return FooHolder.foo;} 

简而言之,无论何时当多个线程共享可变数据的时候,每个读或写数据的线程必须获得一把锁。如果没有同步,则一个线程所做的修改就无法保证被另一个线程所观察到。

 

No.49 避免过多的同步

通常,在同步区域应该做尽可能少的工作。以避免死锁。

大量的同步会引起性能损失。

一个类是否应该做成线程安全(thread-safe)的还是线程兼容(thread-compatible)的,如果类会被用在同步和不同步的环境中,一个合理的方法是同时提供两个版本。有以下指导原则:

1)一种做法是提供一个包装类,实现接口,同事将方法调用转发给内部对象中对应的方法之前执行适当的同步操作。

2)第二种做法适合于那些不是被设计用来扩展或者重新实现的类。它提供一个未同步的类和一个子类,在子类中只包含一些被同步的方法,它们依次调用到超类中对应的方法上。

如果一个类或者一个静态方法,依赖于一个可变的静态域,那么必须要在内部进行同步,即使它往往只用于单线程。这种情况下,对于客户要执行外部同步是不可能的,因为不可能保证其他客户也会执行外部同步。

总之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。同事,限制同步区域内部的工作量。

 

No. 50 永远不要在循环的外面调用wait

wait的标准模式:

synchronized(obj){

while(<condition does not hold>)

     obj.wait();

…//perform action appropriate to condition

}

总是使用wait循环来调用wait方法,永远不要再循环的外面调用wait。在等待之前测试条件,如果条件成立就可以跳过等待。如果条件成立+等待之前notify已经被调用,则无法保证线程总会从等待中醒过来。在等待之后测试,对于确保安全性是必要的。当条件不成立时,有下面一些理由可以使一个线程醒过来:1)另一个线程可能得到了锁,在调用notify到等待线程醒过来的时刻之间,得到锁的线程已经改变了被保护的状态; 2)条件没有成立,有线程意外调用了notify;3)通知线程在唤醒的时候,使用了notifyAll;4)伪唤醒,尽管可能很少。


NO.51 不要依赖于线程调度器
不能让应用程序的正确性依赖于线程调度器。否则,结果得到的应用程序既不健壮也不具有可移植性。作为一个推论,不要依赖Thread.yield或者线程优先级。这些设施都只是影响到调度器,它们可以被用来提高一个已经能够正常工作的系统的服务质量,但永远不应用来“修正”一个原本并不能工作的程序。
编写健壮的、响应良好的、可移植的多线程应用程序的最好办法是,尽可能确保在任何给定时刻只有少量的可运行线程。这种办法采用的主要技术是,让每个线程做少量的工作,然后使用Object.Wait等待某个条件发生,或者使用Thread.sleep睡眠一段时间。

 


NO.52 线程安全性的文档化
每个类都应该清楚地在文档中说明它的线程安全属性。在一个方法的声明中出现synchronized修饰符,这是一个实现细节,并不是导出的API文档的一部分。
一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
   1) 非可变性(immutable)-这个类的实例对于其它客户而言是不变的,不需要外部的同步。参见13条。 
   2)线程程安全的(thread-safe)-这个类的实例是可变的,但是所有的地方都包含足够的同步手段,这些实例可以被并发使用无需外部同步。
   3) 有条件的线程安全(conditionally thread-safe)-这个类(或关联的类)包含有某些方法,它们必须被顺序调用,而不能受到其它线程的干扰,除此之外,这种线程安全级别与上一种情形相同。为了消除被其他线程干扰的可能性,客户在执行此方法序列期间,必须获得一把适当的锁。如HashTable或Vector,它们的迭代器要求外部同步。如:

  1. Hashtable h=...; 
  2. synchronized(h){ 
  3. for(Enumeration e=h.keys();e.hasMoreElements();) 
  4.    f(e.nextElement()); 

    4)线程兼容的(thread-compatible)-在每个方法调用的外围使用外部同步,此时这个类的实例可以被安全的并发使用。如ArrayList或HashMap
    5)线程对立的(thread-hostile)这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围。通常情况下,线程对立的根源在于,这个类的方法要修改静态数据,而这些静态数据可能会影响到其它的线程。
对于有条件的线程安全类,在文档中指明“为了允许方法调用序列以原子方式执行,哪一个对象应被锁住”。


NO.53 避免使用线程组
除了线程、锁和监视器之外,线程系统还提供了一个基本的抽象,即线程组(thread-group)。然而线程组并没有提供太多有用的功能。
一个例外是,当线程组中的一个线程抛出一个未被捕获的异常时,ThreadGroup.uncaughtException方法会被自动调用。“执行环境”使用这个方法,以便用适当的方式来响应未被捕获的异常。


NO.54 保护性地编写readObject方法
编写一个类的readObject方法,相当于编写一个公有的构造函数,无论给它传递一个什么样的字节流,它都必须产生一个有效的实例。下面是缩写健壮的readObject方法的指导原则:
①对于对象引用域必须保持为私有的类,对“将被保存到这些域中的对象”进行保护性拷贝。非可变类的可变组件就属于这一类别。
②对于具有约束条件的类,一定要检查约束条件是否满足,如果不满足的话,则抛出一个InvalidObjectException异常。这些检查应跟在所有的保护性拷贝之后。
③如果在对象图被反序列化之后,整个对象图必须都是有效的,则应该使用ObjectInputValidation接口。
④无论是直接方式还是间接方式,都不要调用类中可被改写的方法。
⑤readResolve方法有可能被用来替代保护性的readObject方法。
不严格地说,readObject是一个“用字节流作为唯一参数”的构造函数。当面对一个人工伪造的字节流的时候,readObject产生的对象会违反它所属的类的约束条件。初步的方法,是在readObject方法进行约束性检查,如下例:

  1. private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{ 
  2.    s.defaultReadObject(); 
  3. //Check that our invariants are satisfied
  4. if(start.compareTo(end)>0) throw new InvalidObjectException(start+" after "+ end); 

对上述的防范仍可进行攻击:伪造一个字节流,这个字节流以一个有效的Period实例所产生的字节流作为开始,然后附加上两个额外的引用,指向 Period实例中的两个内部私有Date域,攻击者通过引用攻击内部域。所以,当一个对象被反序列化的时候,对于客户不应该拥有的对象引用,如果哪个域包含了这样的对象引用,则必须要做保护性拷贝,这是非常重要的。如下例:

  1. private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException{ 
  2.    s.defaultReadObject(); 
  3.    start=new Date(start.getTime()); 
  4.    end=new Date(end.getTime()); 
  5. if(start.compareTo(end)>0) throw new InvlaidObjectException(start+" after "+end); 

 

NO.57 必要时提供一个readResolve方法
无论是 singleton,或是其他实例受控(instance-controlled)的类,必须使用readResolve方法来保护“实例-控制的约束 ”。从本质上来讲,readResovle方法把一个readObject方法从一个事实上的公有构造函数变成一个事实上的公有静态工厂。对于那些禁止包外继承的类而言,readResolve方法作为保护性的readObject方法的一种替代,也是非常有用的。
如下sigleton类:

  1. public class Elvis{ 
  2. public static final Elvis INSTANCE = new Elvis(); 
  3. private Elvis(){ 
  4.       ... 
  5.    } 
  6.    ...//remainder omitted

如果Elvis实例序列化接口,则下面的readResolve方法足以保证它的singleton属性。

  1. private Object readResolve() throws ObjectStreamException{ 
  2. //return the one true elvis and let the GC take care of the Elvis impersonator
  3. return INSTANCE; 

不仅仅对于singleton对象是必要的,readResolve方法对于所有其它的实例受控类(如类型安全枚举类型)也是必需的。
readResolve方法的第二个用法是,就像在第56条建议的那样,作为保护性的readObject方法的一种保守的替代选择。此时,第56条中的readObject方法可以下例的例子替代:

  1. //the defensive readResolve idiom
  2. private Object readResolve() throws ObjectStreamException(){ 
  3. return new Period(start,end); 

对于那些允许继承的类,readResolve方法可能无法替代保护性的readObject方法。如果超类的readResolve方法是final 的,则使得子类实例无法被正常地反序列化。如果超类的readResolve方法是可改写的,则恶意的子类可能会用一个方法改写它,该方法返回一个受损的实例。

  结束语:花了好几个月断断续续地把这本书看完了,期间做了好几个项目,忙了一段时间,看书的进度有些拖延,现在看之前整理的笔记都有点遗忘了,不过没关系,以后可以返回去看看,温故而知新嘛。。。

——————

改动了错别字,添加了我偶尔看到的漏了的几个rules。

感想,effective XXX系列给我的感觉总是啰嗦,看完之后觉得裨益不大,可能和实际问题结合得不是很好,一大堆的文字总是让我觉得眼皮沉重。很感谢有热心的读者写了笔记,虽然看笔记都让我觉得难以消化。可能在等待一个契机才能让我明白书中的真谛,可惜在这么一个速食而且残酷的社会,没有人会允许你花时间prepare自己。

花了2,3天看完笔记+一部分书之后,感觉是浪费了现在的宝贵复习时间。不过复习,也不知道是否徒劳。那么多公司的鄙视,周围人的compare,真的让我陷入depression。

posted @ 2010-10-20 11:37  irischan  阅读(373)  评论(0编辑  收藏  举报