Effective Java读书笔记二三(Java Tips.Day.23)

来源:互联网 发布:nginx lua function 编辑:程序博客网 时间:2024/06/03 23:41

TIP 67 避免过度同步


与TIP 66 不同,本条目讨论相反的问题。过度同步可能会导致性能降低、死锁、甚至不确定的行为。


为了避免活性失败和安全性失败,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制。

  1. 不要调用设计成要覆盖的方法,因为你无法确定最终调用的是哪个子类或父类的实现。
  2. 不要调用以函数对象提供的方法,包括以接口回调形式传入的匿名类。

来看看实例:

public class ForwardingSet<E> implements Set<E> {      private final Set<E> s;      public ForwardingSet(Set<E> s){          this.s = s;      }      public int size() {          return s.size();      }      public boolean isEmpty() {          return s.isEmpty();      }      public boolean contains(Object o) {          return s.contains(o);      }      public Iterator<E> iterator() {          return s.iterator();      }      public Object[] toArray() {          return s.toArray();      }      public <T> T[] toArray(T[] a) {          return s.toArray(a);      }      public boolean add(E e) {          return s.add(e);      }      public boolean remove(Object o) {          return s.remove(o);      }      public boolean containsAll(Collection<?> c) {          return s.containsAll(c);      }      public boolean addAll(Collection<? extends E> c) {          return s.addAll(c);      }      public boolean retainAll(Collection<?> c) {          return c.retainAll(c);      }      public boolean removeAll(Collection<?> c) {          return s.removeAll(c);      }      public void clear() {          s.clear();      }  } public class ObservableSet<E> extends ForwardingSet<E> {      public ObservableSet(Set<E> s) {          super(s);      }      private final List<SetObserver<E>> observers =               new ArrayList<SetObserver<E>>();      public void addObserver(SetObserver<E> observer){          synchronized (observers) {              observers.add(observer);          }      }      public boolean removeObserver(SetObserver<E> observer){          synchronized (observers) {              return observers.remove(observer);          }      }      private void notifyElemetnAdded(E element){          synchronized (observers) {              for(SetObserver<E> observer : observers){                  observer.added(this, element);              }          }      }      @Override      public boolean add(E element) {          boolean added = super.add(element);          if(added)              notifyElemetnAdded(element);          return added;      }      @Override      public boolean addAll(Collection<? extends E> c) {          boolean result = false;          for(E element : c){              result |= add(element);          }          return result;      }  }  public interface SetObserver<E> {    void added(ObservableSet<E> set, E element);  }  

测试类:

public class Test {      public static void main(String[] args) {          ObservableSet<Integer> set =                  new ObservableSet<Integer>(new HashSet<Integer>());          // add 方法调用后会触发notifyElemetnAdded(E element)方法          // 执行 SetObserver added方法          set.addObserver(new SetObserver<Integer>() {              public void added(ObservableSet<Integer> set, Integer element) {                  System.out.println(element);            }          });          for(int i = 0; i < 100; i++){              set.add(i);          }      }  } 

ForwardingSet封装了Set,然后ObservableSet继承了ForwardingSet,并添加了一个观察者列表:
List<SetObserver<E>> , 而SetObserver的方法则用于added之后的回调。

运行Test.main方法,一切正常:

012.........9899Process finished with exit code 0

现在修改一下addObserver里的代码,如果element的值为23,则从set中删除自身这个观察者:

set.addObserver(new SetObserver<Integer>() {        public void added(ObservableSet<Integer> set, Integer element) {             System.out.println(element);      }  }); 

你期望程序会打印0~23,然后观察者会取消预定,无法观察到后续的added事件了。
然而,实际上会打印0~23,但会抛出一个异常:ConcurrentModificationException


问题在于,当notifyElementAdded调用观察者的added方法时,它正处于遍历observers列表的过程中。

这种行为是非法的,即使removeObserver里的代码是在同步块中,可以防止并发的修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的obervers列表。


现在我们尝试一些比较奇特的例子,编写一个试图取消预订的观察者:

public class Test3 {    public static void main(String[] args) {        ObservableSet<Integer> set =            new ObservableSet<Integer>(new HashSet<Integer>());        // Observer that uses a background thread needlessly        set.addObserver(new SetObserver<Integer>() {            public void added(final ObservableSet<Integer> s, Integer e) {                System.out.println(e);                if (e == 23) {                    ExecutorService executor =                        Executors.newSingleThreadExecutor();                    final SetObserver<Integer> observer = this;                    try {                        executor.submit(new Runnable() {                            public void run() {                                s.removeObserver(observer);                            }                        }).get();                     } catch (ExecutionException ex) {                        throw new AssertionError(ex.getCause());                    } catch (InterruptedException ex) {                        throw new AssertionError(ex.getCause());                    } finally {                        executor.shutdown();                    }                }            }        });        for (int i = 0; i < 100; i++)            set.add(i);    }}

这个程序不会抛出异常,但会死锁。后台线程调用s.removeObserver(observer); ,它企图锁定observers,但无法获得该锁,因为主线程已经有锁了(正在遍历)。在这期间,主线程一直在等待后台线程来完成对观察者的删除,这正是造成死锁的原因。


要解决这个问题,可以修改notifyElementAdded方法,

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

给observers拍张”快照”,然后在通知方法中,对“快照”列表遍历通知观察者。这样就不会有异常或死锁了。


事实上,将外来方法的调用移出同步的代码块,还有一种更好的办法。使用Java 1.5提供的并发集合,也可以解决问题:

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

使用并发集合之后,我们再也不需要给addObserver、removeObserver、notifyElemetnAdded三个方法内的代码添加同步。

在同步区域之外被调用的外来方法被称作“开放调用”,除了可以避免死锁,还可以极大的增加并发性。

通常,你应该在同步区域内做尽可能少的工作:获得锁,检查共享数据,do some job,然后放掉锁。


过度同步会增加不必要的性能开销。

如果一个类要并发使用,应该将这个类设计为线程安全的。最好是通过内部同步,还可以获得比外部锁定整个对象更高的并发性。
如果一个类无需并发使用,就不要使用同步,然后建立文档,标明这个类不是线程安全的。


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

.

阅读全文
0 0
原创粉丝点击