欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程安全的集合类--Java Concurrency In Practice C05读书笔记

程序员文章站 2022-03-02 10:33:12
...

[本文是我对Java Concurrency In Practice 5.1的归纳和总结.  转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. ]

synchronized集合

java集合框架提供了多种synchronized集合, 比如Vector, HashTable, Collections的synchronizedXxx方法的返回值等.

synchronized集合是线程安全的, 但不是严格线程安全的. 根据JCIP第二章关于线程安全的定义--线程安全的类无需调用方进行额外的同步--synchronized集合是不满足该定义的. 如果我们将线程安全的定义放宽一些--单次调用对象的方法而无需调用方进行额外的同步, 这样synchronized集合就符合定义了.

为什么要加上单次调用的限定呢? 先看一个例子:

public static Object getLast(Vector list) { 
    int lastIndex = list.size() - 1; 
    return list.get(lastIndex); 
} 

public static void deleteLast(Vector list) { 
    int lastIndex = list.size() - 1; 
    list.remove(lastIndex); 
}

假设Vector对象中含有10个元素, 多线程环境下可能出现这样的场景:

线程1调用getLast方法, 计算得知lastIndex为9, 然后线程失去CPU使用权. 接着线程2调用deleteLast方法, 其lastIndex也为9, 线程2删除了第9个元素. 然后线程1重新获得CPU时间, 线程1会试图获取第9个元素, 但是该元素已经被线程2删除了, 此时将抛出ArrayIndexOutOfBoundsException异常.

从上面的例子可知, 尽管Vector对象是线程安全的, 但是如果对其进行复合操作的话(getLast方法既需要取得最后一个元素的索引, 还需要取得最后一个元素的值--类似这样的操作成为复合操作), 仍然需要调用方同步. 正确的做法应该是:

public static Object getLast(Vector list) {
	synchronized (list) {
		int lastIndex = list.size() - 1;
		return list.get(lastIndex);
	}
}

public static void deleteLast(Vector list) {
	synchronized (list) {
		int lastIndex = list.size() - 1;
		list.remove(lastIndex);
	}
}

之所以用Vector对象做锁, 是因为Vector类的所有方法都是同步方法(其锁为this), 这样调用方使用的锁就同Vector内部使用的锁保持一致了.

迭代是最常见的复合操作, 迭代时调用方也需要进行额外的同步, 以保证整个迭代期间集合没有发生变化. 如:

List list = Collections.synchronizedList(new ArrayList());
// ...
synchronized (list) {
	Iterator i = list.iterator(); // Must be in synchronized block
	while (i.hasNext())
	    foo(i.next());
} 

 

ConcurrentModificationException异常和Fail-Fast机制

迭代集合时, 可能会发生ConcurrentModificationException异常. 每个集合内部都拥有一个名为modCount的成员变量, 如果集合发生了变化, 就会更改modCount的值. 使用Iterator开始迭代时,会将modCount的赋值给expectedModCount, 在迭代过程中, 通过每次比较两者是否相等来判断集合是否在内部或被其它线程修改. 如果expectedModCount和modCount不相等, 将抛出ConcurrentModificationException异常:

if (modCount != expectedModCount)
		throw new ConcurrentModificationException(); 

modCount被声明为volatile, 保证了线程可见性. 单线程环境下迭代时也有可能抛出ConcurrentModificationException异常, 比如在迭代时进行了会更改modCount值的操作:

Iterator<Integer> it = list.iterator();
while(it.hasNext()) {
	System.out.println(it.next());
	// remove操作会导致modCount的值被修改, 从而引发ConcurrentModificationException异常
	list.remove(0);
}

java集合类采用的这种机制被称为Fail-Fast机制--检查状态, 如果没有问题则忽略, 如果有问题就抛出异常. java集合采用这种方式避免同步所带来的开销, 如果确实需要同步, 可以使用synchronized集合, 或者在调用方进行, 或者先同步的clone一份集合然后对clone集合进行迭代--开发者可以进行选择. 使用clone的方式进行迭代的例子:

ArrayList<Integer> listClone = null;
// clone时依然需要同步
synchronized (list) {
	listClone = (ArrayList<Integer>) list.clone();
}
Iterator<Integer> it = listClone.iterator();
while (it.hasNext()) {
	doSomething(it.next());
}

采用clone的方式进行迭代是否合适, 取决于很多因素. 比如集合的size, doSomething的调用时间, 迭代的相对频率等. 如果doSomething方法的调用时间很长, 那么使用clone是比较合适的.

除了显式迭代之外, 调用集合的toString, containsAll, removeAll和retainAll等方法也会导致迭代发生.

 

concurrent集合

Collections.synchronizedXxx方法返回synchronized集合, 查看源码可知, 返回的Synchronized集合对象只是一个包装者, 其对每一个方法都进行同步. 如:

class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
	static final long serialVersionUID = -7754090372962971524L;

	final List<E> list;

	SynchronizedList(List<E> list) {
		super(list);
		this.list = list;
	}

	SynchronizedList(List<E> list, Object mutex) {
		super(list, mutex);
		this.list = list;
	}

	public boolean equals(Object o) {
		synchronized (mutex) {
			return list.equals(o);
		}
	}

	public int hashCode() {
		synchronized (mutex) {
			return list.hashCode();
		}
	}

	public E get(int index) {
		synchronized (mutex) {
			return list.get(index);
		}
	}
	// ....
}

这样的做法对性能有很大的影响, 比如多个线程的并发读操作并不会引起并发错误, 但在Synchronized集合中只能一个线程一个线程的读. 除此之外, javadoc明确要求使用Collections.synchronizedXxx包装一个集合对象后, 就不应该使用原有的集合, 因为使用原有的集合会破坏线程安全. 这样的线程安全依赖于约定, 是不可靠的.

jdk5及后续的版本增加了ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, ConcurrentSkipListMap, ConcurrentSkipListSet等concurrent集合类.