一、线程安全的集合
如果多线程要并发地修改一个数据结构,例如散列表,那么很容易会破坏这个数据结构。例如,一个线程可能要向表中插入一个新元素。加入在调整散列表各个桶之间的链接关系的过程中,被剥夺了控制权。如果另一个线程也开始遍历同一个链表,可能使无效的链接并造成混乱,会抛出异常或陷入死循环。
可以通过提供锁来保护共享数据结构,但是选择线程安全的实现作为替代可能更容易些。阻塞队列就是线程安全的集合。
1>高效的映射表、集合和队列
java.util.concurrent包提供了映射表、有序表和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。
与大多数集合不同,size方法不必再常量时间内操作。确定这样的集合当前的大小通常需要遍历。
集合返回弱一致性的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改,但是,它们不会被同一值返回两次,也不会抛出ConcurrentModificationException异常。
注:与之形成对照的是,集合如果在迭代器构造之后发生改变,java.util包中的迭代器将抛出ConcurrentModificationException异常。
并发的散列映射表,可高效地支持大量的读者和一定数量的写者。默认情况下,假定可以有多达16个写者线程同时执行。可以有更多的写者线程,但是,如果同一时间多于16个,其他线程将暂时被阻塞。可以指定更大数目的构造器,然而,恐怕没有这种必要。
2>写数组的拷贝
CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数,这样的安排是很有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组,但是,集合的数组已经被替换了。因此,旧的迭代器拥有一致的试图,访问它无须任何同步开销。
3>较早的线程安全集合
从Java的初始版本开始,Vector和Hashtable类就提供了线程安全的动态数组和散列表的实现。现在这些类被弃用了,取而代之的是ArrayList和HashMap类。这些类不是线程安全的,而集合库中提供了不同的机制。任何集合类通过使用同步包装器变成线程安全的:
List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());
Map<K,V> synchHashMap = Collections.synchronizedMap(new HashMap<K,V>());结果集合的方法使用锁加以保护,提供了线程的安全访问。
应该确保没有任何线程通过原始的非同步方法访问数据结构。最便利的方法是确保不保存任何指向原始对象的引用,简单地构造一个集合并立即传递给包装器。
如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“客户端”锁定:
最好使用java.util.concurrent包中定义的集合,不适用同步包装器中的。特别是,加入他们访问的是不同的桶,由于ConcurrentHashMap已经精心地实现了,多线程可以访问它们而且不会彼此阻塞。有一个例外是经常被修改的数组列表。在那种情况下,同步的ArrayList可以胜过CopyOnWriteArrayList。
synchronized (synchHashMap) {如果使用foreach循环必须使用同样代码,因为循环使用了迭代器。
Iterator<K> iter = synchHashMap.keySet().iterator();
}
注:如果迭代过程中,别的线程修改集合,迭代器会失效,抛出异常。同步仍然是需要的,因此并发的修改可以被可靠地检查出来。
注:HashTable与ConcurrentHashMap的比较?
相同点: Hashtable 和 ConcurrentHashMap都是线程安全的,可以在多线程环境中运行; key跟value都不能是null
区别: 两者主要是性能上的差异,Hashtable的所有操作都会锁住整个对象,虽然能够保证线程安全,但是性能较差; ConcurrentHashMap内部使用Segment数组,每个Segment类似于Hashtable,在“写”线程或者部分特殊的“读”线程中锁住的是某个Segment对象,其它的线程能够并发执行其它的Segment对象。