searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Java CopyOnWrite容器实现分析

2023-10-27 06:28:55
5
0

CopyOnWrite是一种优化策略,当在并发写时,通过复制出原内容,在新的内容上读写,然后再把新的内容指向原来内容的引用。通过这种策略实现并发时,读写互不干涉,类似读写分离,来提升性能。
CopyOnWrite容器是写时复制容器,实现读无锁,提高读的并发性能。 Java目前提供了两个CopyOnWrite容器,分别为CopyOnWriteArrayList和CopyOnWriteArraySet,CopyOnWriteArraySet的实现完全是通过CopyOnWriteArrayList来实现的,它持有一个final的CopyOnWriteArrayList引用,把方法调用都委托给CopyOnWriteArrayList来调用。

下面只研究CopyOnWriteArrayList的实现:

一、CopyOnWriteArrayList的结构

CopyOnWriteArrayList的结构比较简单,它只包含了两个成员变量:volatile的array对象数组和ReentrantLock对象。volatile的array引用可以保证修改时对其它线程可见,ReentrantLock锁主要运用在对数组的写操作上。

二、主要方法实现分析

1、add的相关方法实现
add有3个方法,一个是直接添加到数组中,一个是添加到数组中的指定位置,还有一个是如果数组中没有元素时才添加成功的方法,主要是给CopyOnWriteArraySet使用,相关代码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//copy一份新的数组,长度是原来的数组长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把元素添加到新数组的最后位置
newElements[len] = e;
//设置数组引用指向新数组
setArray(newElements);
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 {
//创建一份长度是原数组长度+1的新数组
newElements = new Object[len + 1];
//把原来的数组从0-index复制到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
//把原来的数组从index-numMoved复制到新数组中
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//把元素添加到新数组的index位置
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
public boolean addIfAbsent(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// Copy while checking if already present.
// This wins in the most common case where it is not present
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = new Object[len + 1];
for (int i = 0; i < len; ++i) {
if (eq(e, elements[i]))
return false; // exit, throwing away copy
else
newElements[i] = elements[i];
}
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

从上面的代码可以看出,3者的实现大同小异,主要流程如下:
1).首先加锁,然后获取旧数组,复制出一份新数组,然后在新数组上添加元素,最后把对象数组的引用指用新数组;
2).不同的是add(E e)直接把添加的元素添加到新数组的最后位置,而add(int index ,E e)方法在复制旧数组时不同,需要计算出index前后的长度,再复制;addIfAbsent(E e)则是会判断数组中是否已经存在要添加的元素,如果存在返回false。

2、get方法实现
get方法实现很简单,直接从array数组引用的下标找到对应的元素返回即可:

1
2
3
public E get(int index) {
return (E)(getArray()[index]);
}

其它的方法实现也很简单,涉及到更新数组的操作都会加锁和复制新数组。

三、总结

CopyOnWrite是典型的以空间换时间的模型,实现的容器优点和缺点都突出,优点是性能非常高,因为没有锁。但存在两个问题,第一是内存占用会很高,因为在复制出新数组后,旧的数组中的对象并不会马上销毁,如果数组很大,且并发更新很高时,搞不好会出现内存溢出。可以通过一些方法压缩内容,避免这种问题;第二是存在内存一致性问题,虽然最终内存还是一致的,但如果在复制新数组过程中,引用还未指向新数组时,线程访问会出现读不到新的元素。

0条评论
0 / 1000