一、并发List简答介绍
- 因为并发包中只有CopyOnWriteArrayList ,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
CopyOnWriteArrayList 对象里面有一个array数组对象用来存放具体元素,ReentrantLock 独占锁对象用来保证同时只有一个线程对array进行修改。
- 写时复制的线程安全的list需要注意以下几点:
何时初始化list,初始化的list 元素个数为多少,list是有限大小吗?
如何保证线程安全,比如多个线程进行读写时如何保证是线程安全的?
如何保证使用迭代器遍历List时的数据一致性?
二、源码解析
1、初始化:
- 无参构造函数
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
代码内部创建了一个大小为 0 的 Object数组作为Array的初始值。
- 有参构造函数:
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
创建一个 List 其内部元素是入参toCopyIn的副本
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
入参是一个集合,将集合里的元素复制到本List
2、添加元素:
- 上图中的原理类似主要看一下add(E e)
public boolean add(E e) {
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取array
Object[] elements = getArray();
int len = elements.length;
//复制array 到新数组,
Object[] newElements = Arrays.copyOf(elements, len + 1);
//添加元素到新数组
newElements[len] = e;
//使用新数组替换添加前的数组
setArray(newElements);
return true;
} finally {
//释放独占锁
lock.unlock();
}
}
//获取数组array
final Object[] getArray() {
return array;
}
//替换数组
final void setArray(Object[] a) {
array = a;
}
调用add 方法的线程会首先执行代码中获取独占锁的部分,进行锁的获取,如果duoge线程都调用add 方法则只有一个线程会获取到该锁,其他线程会被阻塞挂起直到锁被释放。
所以一个线程获取到锁后,就保证了在该线程添加元素的过程中其他线程不会对array进行修改。
获取锁后,获取数组,获取到数组后,将该数组复制到一个新的数组【将原来的数组长度➕1】,将新增的元素添加到新数组。
使用新数组替换原数组,并在返回前释放锁,由于加了锁,整个add 过程是个原子性的操作,【注意: 添加元素的时候,是在 副本上进行的操作,而不是在原数组上进行操作】。
3、获取指定位置元素:
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
- getArray 和上面的一样的,看一下 get方法:
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
使用get(int index) 获取下标为index 的元素,如果元素不存在则抛出IndexOutOfBoundsException异常。
当线程X 调用get 方法获取指定位置的元素时,分两步: 1、首先获取array数组【一】,2、通过下标访问指定位置的元素【2】。但是我们要注意到整个过程并没有加锁同步。
因为这两部都没有加锁,可能线程X执行完步骤一 之后,在步骤2 之前,另外一个线程y 进行了remove 操作,假设删除元素1 ,remove会首先获取独占锁,然后进行写时复制操作,也就是复制一份当前array数组,然后在复制的数组里删除 1 这个元素,之后让array 指向复制的数组,而此时,array 之前指向的数组的引用计数器为1 而不是0 ,因为线程X 还在使用它,这时线程X 开始执行步骤2,步骤2 操作的数组是 线程Y 删除元素之前的数组。
- 所以虽然Y 已经删除了index 处的元素,但是线程X的步骤还是会返回index 处的锁,这是写时复制策略产生的弱一致性问题。
4、修改指定元素:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(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 oldValue;
} finally {
lock.unlock();
}
}
首先获取独占锁,从而阻止其他线程对array数组进行修改,然后获取当前数组,并调用get方法获取指定位置的元素,如果指定位置的元素值与新值不一致则创建新数组并复制元素,然后在新数组上修改指定位置的元素值并设置新数组到array,如果指定位置的元素值与新值一样,则为了保证volatile的语义,还是需要重新设置array,虽然array 的内容并没有改变。
5、删除元素:
- 删除list中指定元素可以使用上图中的方法,原理大致一样,主要讲一下remove(int index)
public E remove(int index) {
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取数组
Object[] elements = getArray();
int len = elements.length;
//获取指定元素
E oldValue = get(elements, index);
int numMoved = len - index - 1;
//如果删除的是最后一个元素
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
//分两次复制删除后剩余的元素到新数组
Object[] newElements = new Object[len - 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//使用新数组代替老数组
setArray(newElements);
}
return oldValue;
} finally {
//释放锁
lock.unlock();
}
}
类似于新增元素的方法,首先获取独占锁,保证删除数据期间其他线程无法对array进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组,之后使用新数组替换原来的老数组,最后在返回前释放锁。
6、弱一致性的迭代器:
所谓的弱一致性是指返回迭代器后,其他线程对List的删除改对迭代器是不可见的。前面在获取指定位置的元素试提到过。
使用iterator获取迭代器时会返回COWIterator 对象,COWIterator 存储着一个array的快照,snapshot,说snapshot是list的快照的原因是:遍历元素的过程中,其他线程没有对list进行增删改,那么snapshot本身就是list的array,因为它们是引用关系,但是如果遍历期间其他线程对该list进行了增删改,那么snapshot就是快照了,因为增删改后,list里的数组被新数组替代了,这时候老数组被snapshot引用。
这就说明获取迭代器后,使用该迭代器元素时,其他线程对该list进行的增删改不可见。因为操作的其实是两个数组,这就是弱一致性。
- 这个地方和之前的获取指定位置的元素很像。
三、总结
CopyOnWriteArrayList 使用写时复制的策略来保证list的一致性,而 获取 - 修改 - 写入 这三个步骤并不是原子的,所以在增删改的过程中都使用了独占锁,来保证某个某个时间内只有一个线程对list数组进行修改。CopyOnWriteArrayList 还提供了 弱一致性的迭代器从而保证在获取迭代器后,其他线程对list的修改是不可见的。迭代器遍历的数组是一个快照。
CopyOnWriteArraySet 底层使用 CopyOnWriteArrayList 实现的。