读书人

Effective Java学习(并发)之避免过

发布时间: 2013-10-22 16:17:03 作者: rapoo

Effective Java学习(并发)之——避免过度使用同步

? ? ? 现在我们来尝试一些更复杂的例子。假设我们用一个addObserver调用来代替这个调用,用来替换的那个addObserver调用传递了一个打印Integer值得观察者,这个值被添加到了该集合中,如果值为23,这个观察者要将自身删除:

?

?

? ? ? 你可能以为这个程序会打印0-23的数字,之后观察者会取消预订,程序会悄悄的完成它的工作。实际上确实打印出0-23的数字然后抛出异常ConcurrentModificationException。问题在于,当notifyElementAdded调用观察者的added方法时,他正处于遍历objservers列表的过程中。added方法调用可观察集合的removeObserver方法,从而调用observers.remove方法。现在我们有麻烦了。我们正企图在遍历列表的过程中,将一个元素从列表中删除,这是非法的,notifyElementAdded方法中的迭代式在一个同步块中,可以防止并发修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的observers列表。

?

?

? ? ?现在我们要尝试一些比较奇特的例子:我们来编写一个试图取消预定的观察者,但是不直接调用removeObserver,它用另一个线程的服务来完成,。这个观察者使用了一个executor service:

?

? ? ? 这一次我们没有遇到异常,而是遭到死锁。后台线程调用set.removeObserver,他企图锁定observers,但他无法获得该锁,因为主线程已经没有锁了。在这期间,主线程一直在等待后台程序来完成对观察者的删除,这正是造成死锁的原因。

?

?

? ? ? 这个例子是可以编写示范的,因为观察者实际上没有理理由使用后台线程,但是这个问题却是真实的。从同步区域调外来方法,在真实的系统中已经造成了许多死锁,例如GUI工具箱。

?

? ? ? 在前面这两个例子中(异常和死锁),我们都还算幸运的。调用外来方法时,同步区域所保护的资源处于一致状态。假设档同步区域所保护的约束条件暂时无效时,你要从同步区域中调用一个外来方法。由于java程序设计语言的锁是可重入的,这种调用不会死锁。就像在第一个例子中一样,他会产生一个异常,因为调用线程已经有这个锁了,因此当该线程试图再次获得该锁时会成功,尽管概念上不相关的另一项操作正在该锁所保护的数据上进行着。这种失败的后果可能是灾难性的。从本质上来说,这个锁没有尽到他的职责。可再人的锁简化了多线程的面向对象程序的构造,但是他们可能hi将活性失败变成安全性失败。

?

? ? ? 幸运的是,通过将外来方法的调用移出同步代码块来解决这个问题通常并不太难,对于notofyElementAdded方法,这还设计给observers列表拍张“快照“,然后没有锁也可以安全的遍历这个列表了,进过这一修改,前面两个例子运行起来便在也不会出现异常或者死锁了:

?

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

? ? ? 在同步区域之外被调用的外来方法被称作:”开放调用”。除了可以避免死锁之外,开放调用还可以极大的增加并发性。外来方法的运行时间可能会任意长。如果在同步区域内调用外来方法,其他线程对受保护资源的访问就会遭受不必要的拒绝。

?

?

? ? ? 通常,你应该在同步区域内做尽可能少的工作,。获得锁,检查共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个很耗时的动作,则应该设法把这个动作移到同步区域的外面。

?

本篇的第一部分是关于正确性的。接下来,我们要简洁的讨论下性能。虽然自从java平台早期以来,同步的成本已经下降了,但更重要的是,永远不要过度同步,在这个多核的时代,过度同步的实际成本并不是指获取死锁所花费的CPU时间,而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟。过度同步另一个潜在的开销在于,他会限制VM优化代码执行的能力。

?

? ? ? 如果一个可变的类要并发使用,应该使这个类编程线程安全的,通过内部同步,你还可以获得明显比从外部锁定整个对象更高的并发性。否则,就不要在内部同步。让客户在必要的时候从外部同步。在java平台出现的早期,许多类都违背了这个指导方针。例如,StringBuffer实例几乎总是被用于单个线程中,而他们执行的却是内部同步。为此,StringBuffer基本上都有StringBuilder代替,他在java1.5中是个非同步的StringBuffer。当你不确定的时候,就不要同步你的类,而是应该建立文档,说明他不是线程安全的。

?

? ? ? 如果你在内部同步了类,就可以使用不同的方法来实现高并发性,例如分拆锁、分离锁和非阻锁并发控制。

?

? ? ? 如果方法修改了静态域,俺么你也必须同步这个域的访问,即使他往往只用于单个线程。客户要求在这种方法上执行外部同步时不可能的,因为不可能保证其他不相关的客户也会执行外部同步,

?

? ? ? 简而言之,为了避免死锁和数据破坏,千万不要从同步区域内调用外来的方法。更为一般的将,要尽量限制同步区域内部的工作量。当你设计一个可变的类的时候,要考虑一下他们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要多度同步来的更重要,只有当你有足够的理由一定要在内部同步类的时候,才可以这样做,同时还应该将这个决定清楚的写在文档中。

?

?

?

读书人网 >编程

热点推荐