CopyOnWriteArrayList
实现arraylist多线程数据安全的方式
- jdk提供的Collections.SynchronizedList() 所有方法进行添加synchronized块
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
- 使用reentrantLock自己对add时,进行对读写操作加锁
List<String> list = new ArrayList<String>();
ReentrantLock lock = new ReentrantLock();
lock.lock();
list.add("");
lock.unlock();
-
使用读写锁对读多写少的场景进行优化
List<String> list = new ArrayList<String>();
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.writeLock().lock();
list.add("");
lock.writeLock().unlock();
lock.readLock().lock();
list.get(1);
lock.readLock().unlock();
上面的各种加锁方式,或多或少效率上都有问题,效率上最好的读写锁,在大量写操作的时候,读操作也会被阻塞。没法进行读取数据。在业务上有时候,并不需要实时数据,需要的是缓存起来没有修改前的数据,因此引出了CopyOnWriteArrayList 这个类 可以达到在写操作时,可以读取到数据,并不阻塞起来
概念
CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器,再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。
它的优点:
-
并发数据安全,在少量写的操作下,进行读操作,并不阻塞
缺点:
-
多了内存占用: 写数据是copy一份完整的数据,单独进行操作。占用双份内存。 这一点很重要,在现今多线程状态下,如果数据量相当大,但你又使用了该容器,就会导致你堆内存存到副本得数据量超级大,在不确定的情况下,有可能会导致outofmemoryerror,这就有问题了;这个问题可以使用分布式缓存来存副本,降低单机内存带来得问题。
-
数据一致性: 数据写完之后,其他线程不一定是马上读取到最新内容。也就是事务中得幻读。
CopyOnWriteArrayList<String> list2 = new CopyOnWriteArrayList<>();
list2.add("1");
list2.add("2");
list2.add("3");
list2.add("4");
list2.add("5");
list2.add("6");
list2.get(3);
list2.remove(1);
int j = 0;
for (String str : list2){
if (j==0) list2.add("99");
j++;
}
源码分析
CopyOnWriteArrayList 创建基础数组的新副本,这通常成本太高,但可能效率更高,当遍历操作的数量远远超过其他方法时突变,并且在您不能或不想的情况下非常有用同步遍历,但需要排除并发线程。“快照”样式的迭代器方法使用引用迭代器运行时数组的状态创建了。
继续分析成员变量来分析
lock 锁用来在写操作时,只允许一个线程进行操作,保证数据安全性
final transient ReentrantLock lock = new ReentrantLock();
存储数据的数组,当然包括 transient,这个属性在集合中经常使用
private transient volatile Object[] array;
/**
* 将指定的元素追加到此列表的末尾
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock; //获取到锁
lock.lock();
try {
Object[] elements = getArray();//获取到原数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); //复制一个新的数据
newElements[len] = e;//进行操作
setArray(newElements);//并将原数组引用指向新数组
return true;
} finally {
lock.unlock();
}
}
这里没有用动态扩容,原因还是没有必要而且还是会占用多的内存,本来写操作时会复制元素
remove方法
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();
}
}
CopyOnWriteArraySet
而CopyOnWriteArraySet也是基于CopyOnWriteArrayList实现的,在源代码中可以看见
private final CopyOnWriteArrayList<E> al;
从源代码中看只有一个属性就是 CopyOnWriteArrayList,至于怎么保持数据唯一性。
调用的addIfAbsent 方法,这个从名称就能看出来 如果已经存在则覆盖。
public boolean add(E e) {
return al.addIfAbsent(e);
}
以及remove方法都是调用的CopyOnWriteArrayList中的方法进行实现,本身只是对这个对象进行持有。
public boolean remove(Object o) {
return al.remove(o);
}
总结
整个CopyOnWriteArrayList解决就是在写操作时,无法读取的问题,用于读多写少的场景,因为副本的关系,因此在工作中尽量谨慎使用。也可以自己写一个 读写list来保证数据的安全。