同步容器类
同步容器类包括Vector和Hashtable(二者是早期JDK的一部分),还包括JDK1.2中添加的一些相似的类。同步容器类实现线程安全的方式是:将状态封闭起来,并对每个公有方法进行同步,使得每次只有一个线程能访问容器状态。这里解释一下所谓“状态”指的就是成员变量,“封装起来”即将它们设不private,但是通过公有的方法外界仍然可以访问修改类的私有成员,所以要用synchronized将公有方法进行同步,使得每次只有一个线程能访问容器状态。在多线程环境下调用同步容器类自带的所有方法时,实际上都是在串行执行,所以这严重降低并发性和吞吐量。
像List、Set、Map这些原本不是同步容器类,也可以通过Collections.synchronizedXXX工厂方法将其变为同步容器类,即对其公有方法进行同步。
List<Student> students=Collections.synchronizedList(new ArrayList<Student>());
同步容器类的问题
容器上常见的操作包括:迭代访问、跳转(根据指定顺序找到当前元素的下一个元素)、条件运算(先检查再操作Check-And-Act,比如若没有则添加)。
public static Object getLast(Vector vec){
int lastIndex=vec.size()-1;
return vec.get(lastIndex);
}
public static Object deleteLast(Vector vec){
int lastIndex=vec.size()-1;
return vec.remove(lastIndex);
}
大线程的环境下,getLast()函数中第一行代码之后第二行代码之前如果执行了deleteLast(),那么
getLast()继续执行第二行就会抛出ArrayIndexOutOfBoundException。所以要把getLast()和deleteLast()都变成原子操作:
public static Object getLast(Vector vec){
synchronized(vec){
int lastIndex=vec.size()-1;
return vec.get(lastIndex);
}
}
public static Object deleteLast(Vector vec){
synchronized(vec){
int lastIndex=vec.size()-1;
return vec.remove(lastIndex);
}
}
又比如容器上的迭代操作:
for(int i=0;i<vec.size();i++)
doSomething(vec.get(i));
在size()之后get()之前,其他线程可能删除了vec中的元素,同样会导致抛出
ArrayIndexOutOfBoundException。但这并不意味着Vector不是线程安全的,Vector的状态仍然是有效的,而抛出的异常也与其规范保持一致。
正确的做法是在迭代之前对vector加锁:
synchronized(vec){
for(int i=0;i<vec.size();i++)
doSomething(vec.get(i));
}
迭代器与ConcurrentModificationException
使用for或for-each循环对容器进行迭代时,javac内部都会转换成使用Iterator。在对同步容器类进行迭代时如果发现元素个数发生变化,那么hasNext和next将抛出ConcurrentModificationException,这被称为及时失效(fail-fast)。
List<Student> students=Collections.synchronizedList(new ArrayList<Student>());
//可能抛出ConcurrentModificationException
for(Student student:students)
doSomething(student);
为了防止抛出ConcurrentModificationException,需要在迭代之前对容器进行加锁,但是如果doSomething()比较耗时,那么其他线程都在等待锁,会极大降低吞吐率和CPU的利用率。不加锁的解决办法是“克隆”容器,在副本上进行迭代,由于副本被封闭在线程内,其他线程不会在迭代期间对其进行修改。克隆的过程也需要对容器加锁,开发人员要做出权衡,因为克隆容器本身也有显著的性能开销。
隐藏的迭代器
注意,下面的情况会间接地进行迭代操作,也会抛出ConcurrentModificationException:
- 容器的toString()、hashCode()和equals()方法
- containsAll()、removeAll()、retainAll()等方法
- 调用以上方法的方法,比如StringBuildre.append(Object)会调用toString()
- 容器作为另一个容器的元素或者键
- 把容器作为参数的构造函数
并发容器
并发Map
Copy-On-Write容器
public final class ThreeStorage{
private final Set<String> storages=new HashSet<String>();
public ThreeStorage(){
storages.add("One");
storages.add("Two");
storages.add("Three");
}
public boolean isStorage(String name){
return storages.contains(name);
}
}
虽然Set对象是可变的,但从ThreeStorage的设计上来看,Set对象在构造完成后无法对其进行修改。
public class CopyOnWriteArrayList<E>{
private volatile transient Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
public E set(int index, E element) {
//获取重入锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
Object oldValue = elements[index];
//使用的是==而非equals
if (oldValue != element) {
int len = elements.length;
//复制底层数组
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
//把底层数组写回
setArray(newElements);
} else {
setArray(elements);
}
return (E)oldValue;
} finally {
//释放锁
lock.unlock();
}
}
}