上一条,我们知道了并发中要对数据进行安全性保护,防止数据出错,一般是进行同步保护,但做事情不能从一个极端走向另外一个极端,在同步保护的同时,要注意防止过度同步,因为它会导致性能下降、死锁,甚至是其他的一些不确定行为。本条目中的一个醒目观点就是,如果一个方法被同步了,那么,这个同步方法里涉及的有其他方法,那么这些方法不应该是回调,或者可以被重写。举个例子,我们在 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 ,效率大大提高,现在两个的效率不相上下。
由此我们可以得出结论,外部调用的内部集合应避免上锁,而是建立一个快照,然后对快照进行上锁。