这两个都是Java提供的原生写时复制的容器(也就是java.util.concurrent包下的)。CopyOnWrite的基本思路是在修改(包括增加和修改)之前,拷贝出一份快照,对快照进行修改,然后替换原引用。读取操作则没有进行加锁,所以适用于读多写少的场景。另外由于使用了快照模式(迭代器,修改等操作),因此不能保证强一致性,只能保证最终一致性。
CopyOnWriteArraySet是对CopyOnWriteArrayList的包装,所以重点关注一下CopyOnWriteArrayList。
CopyOnWriteArrayList
从增删改查4个方面总结下这个容器(相对ConcurrentHashMap,CopyOnWrite的实现实在是简单太多了。。。)
增
增加方法就2个:
- public boolean add(E e)
- 整体思路比较简单,先获取排他锁(同一时刻只能有一个线程进行修改操作),然后拷贝一份新的数组并插入新的元素(此时读取操作读的还是旧的数组),最后替换引用并释放锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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();//释放排他锁
}
}
- 整体思路比较简单,先获取排他锁(同一时刻只能有一个线程进行修改操作),然后拷贝一份新的数组并插入新的元素(此时读取操作读的还是旧的数组),最后替换引用并释放锁。
- public void add(int index, E element)
- 整体流程和上一个方法基本一致,区别在于进行索引的有效判断,同时检测是不是在数组的最后插入,拷贝的过程是调用System.arraycopy进行分区间拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public 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);//直接同上一个方法,生成len+1长度的数组
else {
newElements = new Object[len + 1];//生成新的空数组
System.arraycopy(elements, 0, newElements, 0, index);//拷贝0~index
System.arraycopy(elements, index, newElements, index + 1,
numMoved);//拷贝index+1 ~ 最后
}
newElements[index] = element;//index位置填充
setArray(newElements);//替换引用
} finally {
lock.unlock();//释放锁
}
}
- 整体流程和上一个方法基本一致,区别在于进行索引的有效判断,同时检测是不是在数组的最后插入,拷贝的过程是调用System.arraycopy进行分区间拷贝。
删
删除操作对外暴露了2个方法,一个是通过索引下标进行删除,一个是找到指定的Object进行删除。
- public E remove(int index)
- 这个方法实现和 add(int index, E element) 基本类似,不做过多解释了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public 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));//形成新的0~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();
}
}
- 这个方法实现和 add(int index, E element) 基本类似,不做过多解释了。
- public boolean remove(Object o)
- 首先获取数组引用,然后尝试寻找元素,找到了则进入删除操作,找不到则直接返回false
1
2
3
4
5public boolean remove(Object o) {
Object[] snapshot = getArray();//获取引用快照
int index = indexOf(o, snapshot, 0, snapshot.length);//尝试寻找(注意,此时并未加锁,如果此时插入了数据或者其他线程插入了数据,是看不到的,需要在删除的时候重新进行查找)
return (index < 0) ? false : remove(o, snapshot, index);//找到了则进行删除(index>=0的情况),找不到则返回false
} - 看一下具体的删除操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34private 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])) {//依次遍历查找,找到则结束if语句块,并更新index
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();
}
}
- 首先获取数组引用,然后尝试寻找元素,找到了则进入删除操作,找不到则直接返回false
改
修改操作相对比较简单,通过索引下标直接更新即可,方法如下:
- public E set(int index, E element)
- 操作也不复杂,加锁后重新拷贝并替换,唯独注意就是元素没有变化,也要重新更新一下引用(注释说是为了确保volatile写语义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public 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();
}
}
- 操作也不复杂,加锁后重新拷贝并替换,唯独注意就是元素没有变化,也要重新更新一下引用(注释说是为了确保volatile写语义)
查
查询操作外部暴露了4个方法,其实内部都是委托给对应的私有方法,只是没有index参数的方法委托的时候传值是0。
- public int indexOf(Object o)
- public int indexOf(E e, int index)
- public int lastIndexOf(Object o)
- public int lastIndexOf(E e, int index)
前两个方法委托给 private static int indexOf(Object o, Object[] elements, int index, int fence) 执行
后两个方法委托给 private static int lastIndexOf(Object o, Object[] elements, int index) 执行
下面整理一下这两个查找方法: - indexOf 只是看一下元素是否为空,为空则找null,不为空则直接进行遍历查找
1
2
3
4
5
6
7
8
9
10
11
12
13private 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;
} - lastIndexOf 和 indexOf 几乎相同,只是倒着查找而已。
1
2
3
4
5
6
7
8
9
10
11
12private static int lastIndexOf(Object o, Object[] elements, int index) {
if (o == null) {
for (int i = index; i >= 0; i--)
if (elements[i] == null)
return i;
} else {
for (int i = index; i >= 0; i--)
if (o.equals(elements[i]))
return i;
}
return -1;
}
其他方法
contains方法,也是直接调用indexOf方法,判断一下索引位置是不是>=0,就不多说了
get方法,直接获取一下数组引用(获取引用之后,其他线程修改,这里也看不到了。。。),然后取对应下标。
addIfAbsent方法,CopyOnWriteArraySet就是基于这个方法实现的。先通过indexOf方法查找,找到直接返回false,找不到则加锁进行添加,添加之前会判断一下引用是否改变,和remove比较相似。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {//引用发生改变,则需要重新查找,如果还是没找到,则继续添加
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)//遍历查找,并且找到了,则返回false
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)//没太明白为什么又要再次查找。。
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}containsAll方法, 实现也比较简单暴力,获取快照后,直接for循环对每一个元素进行判断,就不附代码了。
removeAll方法,也不算复杂,加锁之后遍历元素,不在待删除集合中,则加入新数组中,然后重新拷贝一份即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public boolean removeAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// temp array holds those elements we know we want to keep
int newlen = 0;
Object[] temp = new Object[len];
for (int i = 0; i < len; ++i) {
Object element = elements[i];
if (!c.contains(element))
temp[newlen++] = element;
}
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}retainAll方法,和removeAll方法类似,对应的是两个集合都包含的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public boolean retainAll(Collection<?> c) {
if (c == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (len != 0) {
// temp array holds those elements we know we want to keep
int newlen = 0;
Object[] temp = new Object[len];
for (int i = 0; i < len; ++i) {
Object element = elements[i];
if (c.contains(element))
temp[newlen++] = element;
}
if (newlen != len) {
setArray(Arrays.copyOf(temp, newlen));
return true;
}
}
return false;
} finally {
lock.unlock();
}
}addAllAbsent方法, indexOf(e, cs, 0, added) 采用将遍历过的待添加元素数组进行替换,节省了新开辟数组的空间,这是一个用的很巧的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public int addAllAbsent(Collection<? extends E> c) {
Object[] cs = c.toArray();
if (cs.length == 0)
return 0;
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
int added = 0;
// uniquify and compact elements in cs
for (int i = 0; i < cs.length; ++i) {
Object e = cs[i];
if (indexOf(e, elements, 0, len) < 0 &&
indexOf(e, cs, 0, added) < 0)
cs[added++] = e;
}
if (added > 0) {
Object[] newElements = Arrays.copyOf(elements, len + added);
System.arraycopy(cs, 0, newElements, len, added);
setArray(newElements);
}
return added;
} finally {
lock.unlock();
}
}clear方法,就是设置一个空数组,就不附代码了
addAll方法,也比较简单,转成数组之后直接合并,带索引位置的,就是跳过一部分后,在插入,最后补充剩余的
sort方法,也比较简单,拷贝一份快照后,对快照进行排序,然后替换引用。
equals 和 hashCode ,这两个方法都进行了重写,注意一下,都是使用快照进行比较,所以都是弱一致性的。
iterator 迭代器是创建一个COWIterator迭代器,不支持删除和修改操作,只能进行读取操作。
其他就不进行分析了
CopyOnWriteArraySet
其实没啥可分析的,内部就一个CopyOnWriteArrayList,所有方法都是调用 CopyOnWriteArrayList的方法,去重用的是addIfAbsent和addAllAbsent方法,= =感觉这个Set性能有点差。。。远不及HashSet和TreeSet。
总结
平时项目中时不时还是会用到CopyOnWriteArrayList,CopyOnWriteArraySet至少我还没用到过,而且看了实现,也不太敢用。。。。
回归主题:
- CopyOnWrite相关读取操作都是不加锁的,拷贝一份内部数组的引用,就开始读取了,不用加锁的原因就是所有的数组实际上都不会发生改变,因为所有的修改操作都是生成一份新的数组。
- 因为读取或者迭代器,乃至hashcode,equals方法都是基于快照进行相关操作,所以可能读到的数据并不是最新的,也就是无法保证实时的一致性(就是所谓的弱一致性),但是最终数据还是一致的。
- 因为几乎每次修改,都会生成新的数组,如果写入比较频繁,可能产生大量垃圾,加重GC负担,所以CopyOnWrite最适合的场景就是读多写少。
- CopyOnWriteArrayList源码上有很多值得学习的小技巧,比如addAllAbsent方法中用替换数组中读过的位置存储待合并字符,省去了开辟新数组的开销。