CopyOnWrite

CopyOnWrite 容器

CopyOnWrite 容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行 copy,复制出来一个新的容器,然后向新容器中添加我们需要的元素,最后将原容器的引用指向新容器。

这样做的好处在于,我们可以在并发的场景下对容器进行”读操作”而不需要”加锁”,从而达到读写分离的目的。

CopyOnWrite 机制:当有多个调用者同时去请求一个资源数据的时候,有一个调用者出于某些原因需要对当前的数据源进行修改,这个时候系统将会复制一个当前数据源的副本给调用者修改。

CopyOnWriteArrayList

简介

CopyOnWriteArrayList 经常被用于“读多写少”的并发场景,是因为 CopyOnWriteArrayList 无需任何同步措施,大大增强了读的性能。

在 Java 中遍历线程非安全的 List(如:ArrayList 和 LinkedList)的时候,若中途有别的线程对 List 容器进行修改,那么会抛出 ConcurrentModificationException 异常。CopyOnWriteArrayList 由于其”读写分离”,遍历和修改操作分别作用在不同的 List 容器,所以在使用迭代器遍历的时候,则不会抛出异常。

内存消耗大:CopyOnWriteArrayList 每次执行写操作都会将原容器进行拷贝了一份,数据量大的时候,内存会存在较大的压力,可能会引起频繁 Full GC。

数据不一致:CopyOnWriteArrayList 由于实现的原因,写和读分别作用在不同新老容器上,在写操作执行过程中,读不会阻塞,但读取到的却是老容器的数据。

add() 方法

  1. 使用 synchronized 锁,保证线程安全
  2. 复制原容器
  3. 在新副本上进行写操作
  4. 最后切换引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}

remove() 方法

  1. 使用 synchronized 锁,保证线程安全
  2. 将要 remove 元素之外的其他元素拷贝到新的副本中
  3. 将原容器的引用指向新的副本中
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
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else {
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}

get() 方法

在 CopyOnWriteArrayList 中读操作效率很高,因为没有加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
_    /**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return elementAt(getArray(), index);
__ }_

@SuppressWarnings("unchecked")
static <E> E elementAt(Object[] a, int index) {
return (E) a[index];
}

业务场景

CopyOnWriteArrayList 主要适用于读多写少的业务场景,即读操作非常频繁而写操作相对较少的情况。因为它的读取操作不需要加锁,可以保证高效的并发读性能。

  • 配置管理:在某些系统中,应用的配置数据可能存储在一个列表中,这些配置数据大部分时间是不变的,可能偶尔会被管理员更新。CopyOnWriteArrayList 可以在读取这些配置信息时提供高效的性能。
  • 版本控制:在一些场景下,可能需要快速拍摄某个数据状态的快照来记录历史数据,而不影响当前的数据处理。
  • 定期统计分析:例如,在大数据分析中,可能需要对数据进行定期统计,统计完成后再进行数据刷新。数据统计阶段有大量读操作,而数据刷新阶段则是批量的写操作。