第六十七条 避免过度同步

上一条,我们知道了并发中要对数据进行安全性保护,防止数据出错,一般是进行同步保护,但做事情不能从一个极端走向另外一个极端,在同步保护的同时,要注意防止过度同步,因为它会导致性能下降、死锁,甚至是其他的一些不确定行为。本条目中的一个醒目观点就是,如果一个方法被同步了,那么,这个同步方法里涉及的有其他方法,那么这些方法不应该是回调,或者可以被重写。举个例子,我们在 16 条中举例个 ForwardingSet 集合,那么以它为蓝本,举例

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 (observer) {
            observers.add(observer);
        }
    }

    public boolean removeObserver(SetObserver<E> observer){
        synchronized (observer) {
            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);
        }
    }
}

运行后,没有问题,会打印添加的数字。因为我们重写了集合的add()方法,在添加元素的基础上,如果添加成功,就会调用 notifyElemetnAdded(E element) 方法,注意看这个方法,它里面是个同步锁,功能为遍历添加的监听集合,调用监听,这妥妥的观察者模式啊,我们测试代码中的监听只添加了一个,并且里面的代码是打印这个对象,所以就有了输出台上的信息。上述代码没问题,我们修改一下,加入我们修改 addObserver()方法,比如我们在元素添加到23时,把当前监听取消,会如何呢?

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);
                if(element == 23)
                    set.removeObserver(this);
            }
        });

        for(int i = 0; i < 100; i++){
            set.add(i);
        }
    }
}


这次以,出错了,看看控制台输出

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.duan.rand.ObservableSetTest$ObservableSet.notifyElemetnAdded(ObservableSetTest.java:69)
    at com.duan.rand.ObservableSetTest$ObservableSet.add(ObservableSetTest.java:79)
    at com.duan.rand.ObservableSetTest.main(ObservableSetTest.java:32)

本意是我们打印数字,打印到23时,把这个打印的观察者取消,意思是打印0-23为止,停止打印,程序正常,但此时程序明显与预想不一样,问题出在哪了?我们在 set.add(i); 时,是会调用 notifyElemetnAdded(element) 方法,这个方法,本身就是一个遍历 observers 集合,遍历集合有回调 observer.added(this, element);,我们在这个回调里又调用了set.removeObserver(this); ,意思就是 observers集合把这个监听remove掉,这就变成了同一个ArrayList集合,用增强for循环遍历元素时,又调用remove()方法,但然就会出问题。虽然我们使用了同步锁,可以防止并发修改,但我们无法防止单一线程中本身的同时操作,一边增强for遍历一边删除,所以就出错了。

我们再次做出尝试

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(final ObservableSet<Integer> set, Integer element) {
                System.out.println(element);
                if(element == 23){
                    ExecutorService executor = Executors.newSingleThreadExecutor();
                    final SetObserver<Integer> observer = this;
                    try {
                        executor.submit(new Runnable(){
                            public void run() {
                                set.removeObserver(observer);
                            }
                        }).get();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }finally{
            executor.shutdown();
            }
                }
            }
        });

        for(int i = 0; i < 100; i++){
            set.add(i);
        }
    }
}

在我的电脑上,仍旧报错;在以前旧版本的jvm上,会遇到死锁,后台线程调用set.removeObserver,企图锁定observers,但它无法获得该锁,因为主线程已经有锁了,主线程一直等待后台线程完成对它的删除。对于上述问题,怎么办?两种解决方法

一、我们  notifyElemetnAdded(E element) 方法中,对集合observers做个转换,相当于临时变量,每次调用notifyElemetnAdded(E element) 方法时,都会检查一下observers集合,然后把集合observers的元素通过clone的形式给予snapshot集合,遍历snapshot集合,这样就把遍历和remove()拆分了开来,互不影响

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

二、用系统 CopyOnWriteArrayList 集合代替 ArrayList 集合,这个类通过clone底层数组,实现元素的增删,可以在并发的情况下,保持元素的正确增删。我们可以看一下它的add()实现方法

android 版本源码:
    public synchronized boolean add(E e) {
        Object[] newElements = new Object[elements.length + 1];
        System.arraycopy(elements, 0, newElements, 0, elements.length);
        newElements[elements.length] = e;
        elements = newElements;
        return true;
    }

    public synchronized void add(int index, E e) {
        Object[] newElements = new Object[elements.length + 1];
        System.arraycopy(elements, 0, newElements, 0, index);
        newElements[index] = e;
        System.arraycopy(elements, index, newElements, index + 1, elements.length - index);
        elements = newElements;
    }

    public synchronized E remove(int index) {
        @SuppressWarnings("unchecked")
        E removed = (E) elements[index];
        removeRange(index, index + 1);
        return removed;
    }

java 版本源码

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

不难看出,android 和 java 虽然实现细节不同,但原理一样,都是对这个方法加上锁,然后clone元素到新数组,不同的是,android 用的是关键字 synchronized ,而java用的是ReentrantLock 这个lock锁,在 jdk1.5 之前,synchronized 效率比 lock锁 效率低,但1.5以后,jvm大大优化了 synchronized ,效率大大提高,现在两个的效率不相上下。


由此我们可以得出结论,外部调用的内部集合应避免上锁,而是建立一个快照,然后对快照进行上锁。

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/84580637