本小节是《并发容器》的最后一部分,这一个小节描述的是针对List/Set接口的一个线程版本。html
在《并发队列与Queue简介》中介绍了并发容器的一个归纳,主要描述的是Queue的实现。其中特别提到一点LinkedList是List/Queue的实现,可是LinkedList确实非线程安全的。无论BlockingQueue仍是ConcurrentMap的实现,咱们发现都是针对链表的实现,固然尽量的使用CAS或者Lock的特性,同时都有经过锁部分容器来提供并发的特性。而对于List或者Set而言,增、删操做其实都是针对整个容器,所以每次操做都不可避免的须要锁定整个容器空间,性能确定会大打折扣。要实现一个线程安全的List/Set,只须要在修改操做的时候进行同步便可,好比使用java.util.Collections.synchronizedList(List<T>)或者java.util.Collections.synchronizedSet(Set<T>)。固然也可使用Lock来实现线程安全的List/Set。java
一般状况下咱们的高并发都发生在“多读少写”的状况,所以若是可以实现一种更优秀的算法这对生产环境仍是颇有好处的。ReadWriteLock固然是一种实现。CopyOnWriteArrayList/CopyOnWriteArraySet确实另一种思路。算法
CopyOnWriteArrayList/CopyOnWriteArraySet的基本思想是一旦对容器有修改,那么就“复制”一份新的集合,在新的集合上修改,而后将新集合复制给旧的引用。固然了这部分少不了要加锁。显然对于CopyOnWriteArrayList/CopyOnWriteArraySet来讲最大的好处就是“读”操做不须要锁了。数组
咱们来看看源码。安全
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
public E get(int index) {
return (E)(getArray()[index]);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
public void clear() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
setArray(new Object[0]);
} finally {
lock.unlock();
}
}
对于上述代码,有几点说明:并发
- List仍然是基于数组的实现,由于只有数组是最快的。
- 为了保证无锁的读操做可以看到写操做的变化,所以数组array是volatile类型的。
- get/indexOf/iterator等操做都是无锁的,同时也能够看到所操做的都是某一时刻array的镜像(这得益于数组是不可变化的)
- add/set/remove/clear等元素变化的都是须要加锁的,这里使用的是ReentrantLock。
这里有一段有意思的代码片断。高并发
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
Object oldValue = elements[index];
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return (E)oldValue;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
对于set操做,若是元素有变化,修改后setArray(newElements);将新数组赋值还好理解。那么若是一个元素没有变化,也就是上述代码的else部分,为何还须要进行一个无谓的setArray操做?毕竟setArray操做没有改变任何数据。性能
对于这个问题也是颇有意思,有一封邮件讨论了此问题(1、2、3)。
大体的意思是,尽管没有改变任何数据,可是为了保持“volatile”的语义,任何一个读操做都应该是一个写操做的结果,也就是读操做看到的数据必定是某个写操做的结果(尽管写操做没有改变数据自己)。因此这里即便不设置也没有问题,仅仅是为了一个语义上的补充(我的理解)。ui
这里还有一个有意思的讨论,说什么addIfAbsent在元素没有变化的时候为何没有setArray操做?这个要看怎么理解addIfAbsent的语义了。若是说addIfAbsent语义是”写“或者”不写“操做,而把”不写“操做看成一次”读“操做的话,那么”读“操做就不须要保持volatile语义了。this
对于CopyOnWriteArraySet而言就简单多了,只是持有一个CopyOnWriteArrayList,仅仅在add/addAll的时候检测元素是否存在,若是存在就不加入集合中。
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
return al.addIfAbsent(e);
}
在使用上CopyOnWriteArrayList/CopyOnWriteArraySet就简单多了,和List/Set基本相同,这里就再也不介绍了。
整个并发容器结束了,接下来好好规划下线程池部分,而后进入最后一部分的梳理。