第79条 避免过度同步
在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法
-
死锁的例子:
public class ObservableSet<E> extends ForwardingSet<E> { public ObservableSet(Set<E> set) { super(set); } private final List<SetObserver<E>> observers = new ArrayList<>(); 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 notifyElementAdded(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) notifyElementAdded(element); return added; } @Override public boolean addAll(Collection<? extends E> c) { boolean result = false; for (E element : c) result |= add(element); // Calls notifyElementAdded return result; } } set.addObserver(new SetObserver<>() { public void added(ObservableSet<Integer> s, Integer e) { System.out.println(e); if (e == 23) { ExecutorService exec = Executors.newSingleThreadExecutor(); try { exec.submit(() -> s.removeObserver(this)).get(); } catch (ExecutionException | InterruptedException ex) { throw new AssertionError(ex); } finally { exec.shutdown(); } } } });
这时候优于执行到这里的时候会去原list里面remove,而这时候原list已经被锁了,就会造成死锁
由于 Java 程序设计语言中的锁是可重入的,所以两次进入同一个锁的同步代码块调用不会死锁
CopyOnWriteArrayList
过重新拷贝整个底层数组,进而实现所有的修改操作应该在同步区域内做尽可能少的工作
思考
- 死锁是一定需要注意的,它不会抛出任何异常,只会让所有的线程都处于阻塞状态,后续发现和排查问题都比较困难
-
CopyOnWriteArrayList
虽然可以避免很多多线程问题,但是在对于写操作特别频繁的场景下,CopyOnWriteArrayList
的性能会下滑的非常严重,如果写操作非常多,使用CopyOnWriteArrayList
需要慎重考虑