JUC之CopyOnWriteArrayList和CopyOnWriteArraySet
Posted fondwang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC之CopyOnWriteArrayList和CopyOnWriteArraySet相关的知识,希望对你有一定的参考价值。
一、简介
CopyOnWriteArrayList简介
ArrayList是一种 “列表” 数据结构,其底层是通过数组来实现元素的随机访问。JDK1.5之前,如果想要在并发环境下使用 “列表”,一般有以下3种方式:
1. 使用Vector类
2. 使用Collections.synchronizedList返回一个同步代理类;
3. 自己实现ArrayList的子类,并进行同步/加锁
前两种方式都相当于加了一把“全局锁”,访问任何方法都需要首先获取锁。第3种方式,需要自己实现,复杂度较高。
JDK1.5时,随着JUC引入了一个新的集合工具类——CopyOnWriteArrayList:
大多数业务场景都是一种“读多写少”的情形,CopyOnWriteArrayList就是为适应这种场景而诞生的。
CopyOnWriteArrayList,运用了一种“写时复制”的思想。
通俗的理解就是当我们需要修改(增/删/改)列表中的元素时,不直接进行修改,而是先将列表Copy,然后在新的副本上进行修改,修改完成之后,在将引用从原列表指向新列表。
这样做的好处是读/写是不会冲突的,可以并发进行,读操作还是在原列表,写操作在新列表。仅仅当有多个线程同时进行写操作时,才会进行同步。
CopyOnWriteArraySet简介
CopyOnWriteArraySet,是另一类适合并发环境的SET工具类。从名字上可以看出,也就是基于“写时复制” 的思想。
事实上,CopyOnWriteArraySet内部引用了一个CopyOnWriteArrayList对象,以“组合”方式,委托CopyOnWriteArrayList对象实现了所有API功能。
二、源码分析
CopyOnWriteArraySet基本是依靠CopyOnWriteArrayList的,所以我们只分析CopyOnWriteArrayList即可
CopyOnWriteArrayList
构造器
public CopyOnWriteArrayList() { setArray(new Object[0]);}
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); //setArray方法进行初始化
}public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
属性
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
private static final sun.misc.Unsafe UNSAFE;
private static final long lockOffset;
核心方法
与构造器初始化相关的setArray方法
final void setArray(Object[] a) { array = a; } // 设置底层数据结构数组值
final Object[] getArray(){ return array; } //返回数据
查询——get方法
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
可以看到,get方法并没有加锁,直接返回了内部数组对应索引位置的值:array[index]
添加——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); // 内部array引用指向新数组
return true;
} finally {
lock.unlock(); // 最后释放锁
}
}
// 指定位置添加
public void add(int index, E element) {
final ReentrantLock lock = this.lock; // 获取锁对象
lock.lock(); //加锁
try {
Object[] elements = getArray(); //返回旧数组
int len = elements.length; //旧数组长度
if (index > len || index < 0) // 长度合法验证
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index; //
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1]; // 指定长度新的空数组
System.arraycopy(elements, 0, newElements, 0, index); //将旧数组范围0到index-1位置上数据,赋值给新数组
System.arraycopy(elements, index, newElements, index + 1,
numMoved); // 将旧数组从位置index到最后,赋值给新数组从index+1的位置开始到最后
}
newElements[index] = element; //将新数组index空位置上赋值添加的元素
setArray(newElements); // 内部array引用指向新数组
} finally {
lock.unlock(); //释放锁
}
}
add方法首先会进行加锁,保证只有一个线程能进行修改;然后会创建一个新数组(大小为 n+1
),并将原数组的值复制到新数组,新元素插入到新数组的最后;最后,将字段array
指向新数组。
上图中,ThreadB对Array的修改由于是在新数组上进行的,所以并不会对ThreadA的读操作产生影响。
删除——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();
}
}
// 指定对象删除
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length); // 找出对象的索引,转到对应方法
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWrite的应用场景
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。
CopyOnWrite的缺点
CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
CopyOnWriteArrayList为什么并发安全且性能比Vector好
我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。
参考:https://www.cnblogs.com/myseries/p/10877420.html
https://segmentfault.com/a/1190000016214572
以上是关于JUC之CopyOnWriteArrayList和CopyOnWriteArraySet的主要内容,如果未能解决你的问题,请参考以下文章
JUC 一 CopyOnWriteArrayList 和 CopyOnWriteArraySet