重温《Java并发编程实战》中,有地方引起了我的注意,以前估计读的也是一知半解的略过了,但是现在对多线程有着不一样的体悟之后,经过一段苦思冥想之后,终于想通了。
这边把代码贴出来。
线程不安全的:
class ListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public synchronized boolean putIfAbsent(E x) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
线程安全的:
@ThreadSafe
class ListHelper <E> {
public List<E> list = Collections.synchronizedList(new ArrayList<E>());
public boolean putIfAbsent(E x) {
synchronized (list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}
一开始很诧异为啥上面的线程不安全,下面的线程安全。
不过想通了就很好解释了,下面是我的思考:
这个方法的作用是,如果没有则添加,所以就必须保证list 这个变量的正确性。
假设有一个线程A,一个线程B,都有同一个ListHelper 对象,锁住了putIfAbsent方法,乍一看是没什么问题的,但是仔细想想。sychronized获取到的只是这个ListHelper 对象的监视锁,假设线程A获取到了这个锁,那么线程B不能进入putIfAbsent方法是很正常的,可以理解的,但是假设这类中有一个其他的方法对list进行修改,且该方法并未被sychronized锁住,线程B是可以进入这个方法的,那么很明显,list没有可见性的,从而就会导致不正确性。那么这个sychronized是没起到保证list正确性的,因此是线程不安全的。
还有一个情况是,假设该list并没有被Collections.synchronizedList修饰的话,锁住的也只是list对象,也就是说,还是和上面差不多,因为ArrayList是线程不安全的,里面的方法并没有被sychronized修饰,这样的话就会导致还是可以在其他方法中修改该list的。
但是使用了Collections.synchronizedList就不一样了。下面看一下源码:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
可以看到,实现了RandomAccess 接口的会返回一个SynchronizedRandomAccessList类,否则返回SynchronizedList类。由于ArrayList是实现了这个接口,因此看一下SynchronizedRandomAccessList这个类:
static class SynchronizedRandomAccessList<E>
extends SynchronizedList<E>
implements RandomAccess {
SynchronizedRandomAccessList(List<E> list) {
super(list);
}
SynchronizedRandomAccessList(List<E> list, Object mutex) {
super(list, mutex);
}
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedRandomAccessList<>(
list.subList(fromIndex, toIndex), mutex);
}
}
private static final long serialVersionUID = 1530674583602358482L;
private Object writeReplace() {
return new SynchronizedList<>(list);
}
}
会发现这个类继承自SynchronizedList,然后subList方法中使用mutex监视器锁(其实每个方法都会锁住mutex,只是这里没贴出来),但是这个mutex并没有在其中定义,这时来看看构造方法:
SynchronizedRandomAccessList(List<E> list, Object mutex) {
super(list, mutex);
}
这里有个mutex变量,点进去看,会发现该变量定义在SynchronizedCollection中,那么再来看一下SynchronizedCollection的部分代码:
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
}
可以看到这里定义了变量mutex,同时这个mutex在构造方法中赋值为this。因此,在线程安全的ListHelper 中锁住的list,与内部锁一致,因此是线程安全的。
通过上面的源码,就可以知道线程安全的ListHelper 安全在哪了,使用之前对线程不安全的ListHelper 分析来线程安全的ListHelper ,会发现之前线程不安全的原因在这个类中不存在了。
以上,就是我对Collections.synchronizedList的部分思考,如有错误,欢迎指教。