[本文是我对Java Concurrency In Practice 5.2的归纳和总结. ?转载请注明作者和出处, ?如有谬误, 欢迎在评论中指正. ]
ConcurrentHashMap类
我们可以使用Collections.synchronizedMap()方法包装HashMap得到线程安全的Map, 但是如前所述, 这样会带来很大的性能损失. JDK5之后我们有了新的选择--ConcurrentHashMap. ConcurrentHashMap具有如下特点:
1. ConcurrentHashMap具有更好的并发性能. ConcurrentHashMap是线程安全的, 但是其同步策略和SynchronizedMap有很大不同. ConcurrentHashMap在read时几乎不用加锁, 而write时使用的是细粒度的分段锁, ConcurrentHashMap甚至可以做到并发write.?
2. 由于ConcurrentHashMap的分段加锁机制, 使用ConcurrentHashMap类时, 调用方无法再自行加锁.
3. 由于调用方无法自行加锁, 因此ConcurrentHashMap类提供了一些常见的复合操作. 如put-If-Absent,remove-if-equal, replace-if-equal等:
// 只有key不是集合中的键时才插入该键值对
V putIfAbsent(K key, V value);?
// 集合中存在该键值对时才删除
boolean remove(K key, V value);?
// 只有key和oldValue是集合中的键值对是才进行替换
boolean replace(K key, V oldValue, V newValue);?
// 只有key是集合中的key时才进行替换
V replace(K key, V newValue);
4. 迭代时不需要调用方进行额外的同步. ConcurrentHashMap使用的迭代器被称为weakly consistent(弱一致)迭代器, 弱一致迭代器不会在迭代期间抛出ConcurrentModificationException异常. 迭代开始后, 如果其他线程删除了ConcurrentHashMap集合的某个元素, 且被删除的元素尚未由next方法返回, 则该元素就不会被迭代器返回给调用方. 如果迭代开始后其他线程往ConcurrentHashMap集合中插入了新的元素, 那么新的元素可能会也可能不会被返回给调用方. 无论如何, 弱一致迭代器都保证不会将同一个元素多次返回给调用方.?
5. 调用ConcurrentHashMap对象的size, isEmpty等方法时(这些方法是针对整体Map的操作), 性能比较差. ConcurrentHashMap适合在要求高并发高性能的场合下使用, 在这些场景下, size或者isEmpty等方法用处不大, 这是可以接受的权衡.
ConcurrentHashMap的实现机制可参考http://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/index.html?ca=drs-
除了确实需要额外加锁的场景, ConcurrentHashMap都是比SynchronizedMap更好的选择.
?
CopyOnWriteArrayList类
同ArrayList一样, CopyOnWriteArrayList底层使用数组存储数据. CopyOnWriteArrayList中的数组定义为volatile, 以保证线程间的可见性. CopyOnWriteArrayList处理写操作(add, remove, set)时, 都会先copy一份数组, 然后在新的数组上进行写操作. 例如add方法:
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { // getArray返回的原先的底层数组 Object[] elements = getArray(); int len = elements.length; // copy数组中的数据 Object[] newElements = Arrays.copyOf(elements, len + 1); // 加入新数据 newElements[len] = e; // 改变底层数组 setArray(newElements); return true; } finally { lock.unlock(); } }
这样的写操作肯定会导致性能大幅下降, 尤其是底层数组中包含很多数据的时候. 但是会带来一个优点: 处理读操作(get, iterator, contains等)时不需要进行同步和加锁. 由于读操作是针对当前的数组进行的, 如果读操作过程中其他线程并发修改了CopyOnWriteArrayList对象, 不会影响到当前数组. 所以读操作肯定是线程安全的:
public E get(int index) { return (E)(getArray()[index]); } public boolean contains(Object o) { Object[] elements = getArray(); return indexOf(o, elements, 0, elements.length) >= 0; }
迭代也是读操作操作的一种:
private static class COWIterator<E> implements ListIterator<E> { // 底层数组的快照 private final Object[] snapshot; // 游标 private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } public boolean hasPrevious() { return cursor > 0; } public E next() { if (!hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } public E previous() { if (!hasPrevious()) throw new NoSuchElementException(); return (E) snapshot[--cursor]; } public int nextIndex() { return cursor; } public int previousIndex() { return cursor - 1; } /** * 迭代器不支持remove操作 */ public void remove() { throw new UnsupportedOperationException(); } /** * 迭代器不支持set操作 */ public void set(E e) { throw new UnsupportedOperationException(); } /** * 迭代器不支持add操作 */ public void add(E e) { throw new UnsupportedOperationException(); } }
创建迭代器时, 迭代器就初始化了当前数组的快照. 就算迭代期间进行了写操作, 也不会影响到迭代器中的snapshot数组. 所以CopyOnWriteArrayList返回的迭代器只反应迭代发生时CopyOnWriteArrayList对象所持有的集合, 迭代期间发生的改变不会反应出来.
总结:
1. CopyOnWriteArrayList是线程安全的, 且处理读操作不需要进行同步和加锁. 所以读操作具有很好的并发性.
2. CopyOnWriteArrayList的写操作是代价很大的, 所以CopyOnWriteArrayList只适用于读操作频率远远大于写操作频率的场景.
3. CopyOnWriteArrayList无法在调用方进行额外加锁. 同时CopyOnWriteArrayList也提供了一些常用的复合操作, 如putIfAbsent等.
4. CopyOnWriteArrayList的迭代只能反应迭代开始时CopyOnWriteArrayList对象所持有的集合. 迭代期间不会抛出ConcurrentModificationException异常, 调用方不需要进行额外的加锁(实际上也没有进行).
CopyOnWriteArraySet和CopyOnWriteArrayList具有类似的特点.
?