并发包中的并发List只有CopyOnWriteArrayList。
实现原理:利用写时复制策略,在底层进行修改操作的时候都是通过复制的数组(快照)上进行。
如何控制并发安全:
上述类图中,每个CopyOnWriteArrayList对象里面有一个array数组对象用来存放具体元素,ReentrantLock独占锁对象用来保证同时只有一个线程对array修改。
如何自己实现一个写时复制的线程安全的list,需要注意哪些点?
何时初始化list, 初始化list元素个数以及是否有限,动态扩容之类的(与arrayList一样)
如何保证线程安全,比如多个线程进行读写时候如何保证线程安全的(多线程读写)
如何保证使用迭代器遍历list的数据一致性(遍历数据一致性)
源码—初始化
源码—添加元素
源码—获取指定元素
// 位置访问操作
/**
* {@inheritDoc}
*元素不存在抛出数组下标越界异常
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
第一步
final Object[] getArray() {
return array;
}
第二步
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
获取下标方法操作的时候,第一步先获取数组对象,第二步通过下标访问指定的元素。但是整个过程没有加锁同步。这里就会出现写时候复制策略的弱一致性问题。
set方法
/**
替换指定的集合中的元素
*
* 抛出数组越界的异常如果不存在
*/
public E set(int index, E element) {
//获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取数组并调用get方法获取当前位置的数组元素
Object[] elements = getArray();
E oldValue = get(elements, index);
//如果指定的元素与之前的元素值不一样,则copy一份新的数组并将该元素添加到指定的位置
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
//如果指定位置元素与新值一样,则为了保证volatile语义,还是需要重新设置数组
setArray(elements);
}
//返回指定位置的原值
return oldValue;
} finally {
//释放独占锁
lock.unlock();
}
}
删除指定元素
/**
* 移除list中指定的元素
将其后面的元素统一向左移动
返回被移除的值
*
* 没有则抛出 IndexOutOfBoundsException 异常
*/
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 {
如果不是最后一个元素就copy两个数组
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();
}
}
小结:
通过写时复制策略来保证list的一致性,而获取-修改-写入操作并不是原子性的,所以在增删改的过程中使用了独占锁,来保证同一个时刻,只有一个线程能对list数组进行修改。但是也正因为如此,迭代器遍历的时候会出现弱一致性问题(使用时候需要特别注意)