EffectiveJava第十章第二节

避免过度同步

过度同步所导致的最明显问题就是性能下降,特别是在如今的多核时代,再有就是可能引发的死锁和一系列不确定性的问题。当同步函数或同步代码块内调用了外来方法,如可被子类覆盖的方法,或外部类的接口方法等。由于这些方法的行为存在一定的未知性,如果在同步块内调用了类似的方法,将极有可能给当前的同步带来未知的破坏性。见如下使用观察者模式实现一个向集合容器添加元素时发出通知的例子代码:

import java.util.*;  
 
 
public class ForwardingSet implements Set{  
   private final Set s;  
     
   public ForwardingSet(Set s){  
       this.s = s;  
   }  
 
   @Override  
   public int size() {return this.s.size();}  
     
   @Override  
   public void clear() {this.s.clear();}  
     
   @Override  
   public boolean isEmpty() {return this.s.isEmpty();}  
 
   @Override  
   public boolean contains(Object o) {return this.s.contains(o);}  
 
   @Override  
   public Iterator iterator() {return this.s.iterator();}  
 
   @Override  
   public Object[] toArray() {return this.s.toArray();}  
 
   @Override  
   public  T[] toArray(T[] a) {return this.s.toArray(a);}  
 
   @Override  
   public boolean add(E e) {return this.s.add(e);}  
 
   @Override  
   public boolean remove(Object o) {return this.s.remove(o);}  
 
   @Override  
   public boolean containsAll(Collection c) {return this.s.containsAll(c);}  
 
   @Override  
   public boolean addAll(Collection c) {return this.s.addAll(c);}  
 
   @Override  
   public boolean retainAll(Collection c) {return this.s.removeAll(c);}  
 
   @Override  
   public boolean removeAll(Collection c) {return this.s.retainAll(c);}  
} 
import java.util.ArrayList;  
import java.util.Collection;  
import java.util.List;  
import java.util.Set;  
 
//被观察的集合主题
public class ObservableSet extends ForwardingSet{  
 
   public ObservableSet(Set s) {  
       super(s);  
   }  
     
   //观察者集合  
   private final List>  observers =   
       new ArrayList>();  

   //注册观察者 
   public void addObserver(SetObserver observer){  
       synchronized (observers) {  
           observers.add(observer);  
       }  
   }  
     
   //取消注册观察者
   public boolean removeObserver(SetObserver observer){  
       synchronized (observers) {  
           return observers.remove(observer);  
       }  
   }  
     
   //通知集合中添加元素 
   private void notifyElementAdded(E element){  
       synchronized (observers) {  
           for(SetObserver observer : observers){  
               //调用观察者的方法,告诉观察者添加了元素
               observer.added(this, element);  
           }  
       }  
   }  
 
   @Override  
   public boolean add(E element) {  
       //调用父类方法 
       boolean added = super.add(element);  
       if (added) {  
           notifyElementAdded(element);  
       }  
       return added;  
   }  
 
   @Override  
   public boolean addAll(Collection c) {  
       boolean result = false;  
       for(E element : c){  
           result |= add(element);  
       }  
       return result;  
   }  
}  

Observer通过addObserver方法预定通知,通过调用removeObserve方法取消预定。在这两种情况下,这个回调接口实例会被传递给方法:

//集合观察者  
public interface SetObserver {  
   void added(ObservableSet set, E element);  
}  

如果粗略的检验一下,ObserverSet会显得很正常。例如,下面的程序打印0-99的数字:

public static void main(String[] args) {  
       ObservableSet set = new ObservableSet(new HashSet());  
       set.addObserver(new SetObserver() {  
             
           @Override  
           public void added(ObservableSet set, Integer element) {  
               System.out.print(element+" ");  
           }  
       });  
         
       for(int i = 0; i < 100; i++){  
           set.add(i);  
       }  
   }  

现在我们来尝试一些更复杂的例子。假设我们用一个addObserver调用来代替这个调用,用来替换的那个addObserver调用传递了一个打印Integer值的观察者,这个值被添加到了该集合中,如果值为23,这个观察者要将自身删除:

set.addObserver(new SetObserver() {  
             
           @Override  
           public void added(ObservableSet set, Integer element) {  
               System.out.print(element+" ");  
               if (element == 23) {  
                   set.removeObserver(this);  
               }  
           }  
       });  

你可能以为这个程序会打印0-23的数字,之后观察者会取消预订,程序会悄悄的完成它的工作。实际上确实打印出0-23的数字然后抛出异常ConcurrentModificationException。问题在于,当notifyElementAdded调用观察者的added方法时,他正处于遍历objservers列表的过程中。added方法调用可观察集合的removeObserver方法,从而调用observers.remove方法。现在我们有麻烦了。我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的,notifyElementAdded方法中的迭代式在一个同步块中,可以防止并发修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。

现在我们要尝试一些比较奇特的例子:我们来编写一个试图取消预定的观察者,但是不直接调用removeObserver,它用另一个线程的服务来完成,。这个观察者使用了一个executor service

set.addObserver(new SetObserver() {  
             
           @Override  
           public void added(final ObservableSet set, Integer element) {  
               System.out.print(element+" ");  
               if (element == 23) {  
                   ExecutorService executorService = Executors.newSingleThreadExecutor();  
                     
                   final SetObserver observer = this;  
                   try {  
                       executorService.submit(new Runnable() {  
                           @Override  
                           public void run() {  
                               set.removeObserver(observer);  
                           }  
                       }).get();  
                   } catch (ExecutionException ex) {  
                       throw new AssertionError(ex.getCause());  
                   }catch (InterruptedException ex) {  
                       throw new AssertionError(ex.getCause());  
                   }finally{  
                       executorService.shutdown();  
                   }  
               }  
           }  
       });  

这一次我们没有遇到异常,而是遭到死锁。后台线程调用set.removeObserver,他企图锁定observers,但他无法获得该锁,因为主线程已经没有锁了。在这期间,主线程一直在等待后台程序来完成对观察者的删除,这正是造成死锁的原因。

这个例子是可以编写示范的,因为观察者实际上没有理理由使用后台线程,但是这个问题却是真实的。从同步区域调外来方法,在真实的系统中已经造成了许多死锁,例如GUI工具箱。

在前面这两个例子中(异常和死锁),我们都还算幸运的。调用外来方法时,同步区域所保护的资源处于一致状态。假设当同步区域所保护的约束条件暂时无效时,你要从同步区域中调用一个外来方法。由于java程序设计语言的锁是可重入的,这种调用不会死锁。就像在第一个例子中一样,他会产生一个异常,因为调用线程已经有这个锁了,因此当该线程试图再次获得该锁时会成功,尽管概念上不相关的另一项操作正在该锁所保护的数据上进行着。这种失败的后果可能是灾难性的。从本质上来说,这个锁没有尽到他的职责。可再人的锁简化了多线程的面向对象程序的构造,但是他们可能hi将活性失败变成安全性失败。

幸运的是,通过将外来方法的调用移出同步代码块来解决这个问题通常并不太难,对于notofyElementAdded方法,这还设计给observers列表拍张“快照“,然后没有锁也可以安全的遍历这个列表了,进过这一修改,前面两个例子运行起来便在也不会出现异常或者死锁了:

private void notifyElementAdded(E element) {  
       List> snapshot = null;  
         
       synchronized (observers) {  
           snapshot = new ArrayList>(observers);  
       }  
         
       for(SetObserver observer : snapshot){  
           observer.added(this, element);  
       }  
   } 

事实上,要将外来的方法调用移出同步代码块,还有一种更好的方法。自java1.5之后,java类库就提供了一个并发集合,称作:CopyOnWriteArrayList,这是专门为此定制的。这是ArrayList的一种变体,通过重新拷贝整个底层数组,在这里实现所有的写操作。由于内部数组永远不会改动,因此迭代不需要锁定,速度也非常快。如果大量的使用,CopyOnWriteArrayList的性能将大受影响,但是对于观察者列表来说却是很好的,因为他们几乎不改动,并且经常遍历。

如果这个列表改成使用CopyOnWriteArrayList,就不必改动ObservableSet的add和addAll方法,下面是这个类的其余代码。注意其中并没有任何显示的同步。

private final List> observers = new CopyOnWriteArrayList>();  
     
   public void addObserver(SetObserver observer) {  
       observers.add(observer);  
   }  
 
   public boolean removeObserver(SetObserver observer) {  
       return observers.remove(observer);  
   }  
 
   private void notifyElementAdded(E element) {  
       for (SetObserver observer : observers) {  
               observer.added(this, element);  
       }  
} 

减少不必要的代码同步还可以大大提高程序的并发执行效率,一个非常明显的例子就是StringBuffer,该类在JDK的早期版本中即以出现,是数据操作同步类,即时我们是以单线程方式调用该类的方法,也不得不承受块同步带来的额外开销。Java在1.5中提供了非同步版本的StringBuilder类,这样在单线程应用中可以消除因同步而带来的额外开销,对于多线程程序,可以继续选择StringBuffer,或者在自己认为需要同步的代码部分加同步块。

简而言之,为了避免死锁和数据破坏,千万不要从同步区域内调用外来的方法。更为一般的将要尽量限制同步区域内部的工作量。当你设计一个可变的类的时候,要考虑一下他们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要过度同步来的更重要,只有当你有足够的理由一定要在内部同步类的时候,才可以这样做,同时还应该将这个决定清楚的写在文档中。

你可能感兴趣的:(EffectiveJava第十章第二节)