Effective Java读书笔记二三(Java Tips.Day.23)
来源:互联网 发布:nginx lua function 编辑:程序博客网 时间:2024/06/03 23:41
TIP 67 避免过度同步
与TIP 66 不同,本条目讨论相反的问题。过度同步可能会导致性能降低、死锁、甚至不确定的行为。
为了避免活性失败和安全性失败,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制。
- 不要调用设计成要覆盖的方法,因为你无法确定最终调用的是哪个子类或父类的实现。
- 不要调用以函数对象提供的方法,包括以接口回调形式传入的匿名类。
来看看实例:
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,然后放掉锁。
过度同步会增加不必要的性能开销。
如果一个类要并发使用,应该将这个类设计为线程安全的。最好是通过内部同步,还可以获得比外部锁定整个对象更高的并发性。
如果一个类无需并发使用,就不要使用同步,然后建立文档,标明这个类不是线程安全的。
简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,要尽量限制同步区域内部的工作量。
.
- Effective Java读书笔记二三(Java Tips.Day.23)
- Effective Java读书笔记二二(Java Tips.Day.22)
- Effective Java读书笔记二(Java Tips.Day.2)
- Effective Java读书笔记二十(Java Tips.Day.20)
- Effective Java读书笔记二一(Java Tips.Day.21)
- Effective Java读书笔记二四(Java Tips.Day.24)
- Effective Java读书笔记二五(Java Tips.Day.25)
- Effective Java读书笔记二六(Java Tips.Day.26)
- Effective Java读书笔记一(Java Tips.Day.1)
- Effective Java读书笔记五(Java Tips.Day.5)
- Effective Java读书笔记六(Java Tips.Day.6)
- Effective Java读书笔记八(Java Tips.Day.8)
- Effective Java读书笔记九(Java Tips.Day.9)
- Effective Java读书笔记十(Java Tips.Day.10)
- Effective Java读书笔记十一(Java Tips.Day.11)
- Effective Java读书笔记十二(Java Tips.Day.12)
- Effective Java读书笔记十三(Java Tips.Day.13)
- Effective Java读书笔记十四(Java Tips.Day.14)
- 电影《南京 南京》对于角川最后自杀的一点个人见解
- RabbitMQ与Spring整合之消息生产方
- Java 8之Lambda表达式
- windows7下tomcat环境安装
- Spring AspectJ切入点语法详解
- Effective Java读书笔记二三(Java Tips.Day.23)
- 关于jxl导出
- 【python】subprocess模块中的Popen与call的区别
- Java设计模式透析之 —— 策略(Strategy)
- Unicode 代码复制到 VS 里无法识别的问题
- 对 Linux 专家非常有用的 20 个命令
- Java+maven+selenium+testng+reportNG+jenkins自动化环境搭建【全网最详细的搭建过程指导】
- Aptana Studio3 解决unresolved import的问题
- Map的三个遍历方法