在JDK1.2之前同步容器类包括Vector、HashTable,这两个容器通过内置锁synchronized保证了同步。后面的ArrayList、LinkedList、HashMap、LinkedHashMap等等都不是线程安全的,没有添加同步机制。但是JDK后面的Collections类也提供了这些常见容器类的同步容器类实现:将他们的状态封装起来,并对每个公共方法都进行同步,使得每次只有一个线程能访问容器状态。
Collections类
在Collections这个类下面提供了一些静态类和静态方法。比如常见的同步容器类的创建方法:
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
.....
通过这些静态方法将一个传递进去的集合包装成同步的集合。这里很明显就用到了设计模式中的装饰器模式。
装饰器接受一个接口对象,并返回一个同样接口的对象,不过,新对象可能会扩展一些新的方法或属性,扩展的方法或属性就是所谓的”装饰”,也可能会对原有的接口方法做一些修改,达到一定的”装饰”目的。
Collections有三组装饰器方法,它们的返回对象都没有新的方法或属性,但改变了原有接口方法的性质,经过”装饰”后,它们更为安全了,具体分别是写安全、类型安全和线程安全,这里我们只介绍线程安全的装饰类。
由于篇幅有限,我这里只举一个例子,就是:synchronizedList(List<T> list);
包装的List的线程安全的实现。
老规矩,还是来看源码:
Collections.synchronizedList(new ArrayList<String>());
看看这个static方法的调用发生了什么?
//synchronizedList函数源码
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
我们发现这个函数源码里面实际上是new了一个SynchronizedList<>(list))对象,该对象是collections的内置静态类。并传入参数list。 继续向下看看这个静态类的构造器:
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
其实就是一个传参的过程。下面看看SynchronizedList这个类的属性:
final List<E> list;
一个final修饰的list,表示list一旦传递了引用就不可变。也就是说原本的参数list引用传递给SynchronizedList的list并不可变。
然后我们看一下get()函数的源码:
public E get(int index) {
synchronized (mutex) {
}
}
这个时候就看到了同步的实现了,通过synchronized加锁实现同步代码块,使得每次只有一个线程能访问容器状态。这里的锁对象mutex是来源于父类SynchronizedCollection。看下源码就知道SynchronizedList的所有公共方法都是通过给mutex加锁实现的容器状态访问的同步操作。
其余的同步容器类的实现都是类似的,就不一一分析了。
同步容器类的迭代的线程安全
如果同步容器类的数据量很大,迭代的时候占用时间较长。迭代过程中可能会出现别的线程修改了容器的数据,这样迭代的时候可能会抛出异常(比如迭代到索引是最后一个元素的元素,这时候另外一个线程删除了这个线程,这时候会抛出数组越界的异常)。
但是如果迭代过程中加锁,那么就会出现性能问题。其中一种解决方案就是克隆容器,再迭代克隆的容器。(克隆期间需要加锁),但是克隆也是耗费CPU性能的。所以没有十全十美的办法。