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

本文探讨了过度同步可能导致的性能下降、死锁及不确定行为等问题,并通过具体示例展示了如何正确地进行同步操作,以确保程序的稳定性和高效性。

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方法,一切正常:

0
1
2
...
...
...
98
99

Process 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,然后放掉锁。


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

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


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

.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值