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方法,一切正常:
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,然后放掉锁。
过度同步会增加不必要的性能开销。
如果一个类要并发使用,应该将这个类设计为线程安全的。最好是通过内部同步,还可以获得比外部锁定整个对象更高的并发性。
如果一个类无需并发使用,就不要使用同步,然后建立文档,标明这个类不是线程安全的。
简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法,要尽量限制同步区域内部的工作量。
.
本文探讨了过度同步可能导致的性能下降、死锁及不确定行为等问题,并通过具体示例展示了如何正确地进行同步操作,以确保程序的稳定性和高效性。

被折叠的 条评论
为什么被折叠?



