前言
List 接口在 Java 里是非常常用的,常见的 List 接口实现类如 ArrayList,LinkedList,它们在各种场合都有着广泛的作用。然而这两个List 都是线程不安全的。本文将介绍一种线程安全的 ArrayList:CopyOnWriteArrayList,还是深入其源码进行分析和解释。看之前可以先看作者之前这篇博客:ArrayList源码学习(一):初始化,扩容以及增删改查,对 ArrayList 的基本操作和原理有一个认识。
基本结构和初始化
基本结构
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/**
* The lock protecting all mutators. (We have a mild preference
* for builtin monitors over ReentrantLock when either will do.)
*/
// 作者们的癖好
final transient Object lock = new Object();
// 存数据的数组
private transient volatile Object[] array;
}
复制代码
首先看 CopyOnWriteArrayList 的基本结构,代码如上。可以看出它就两个属性,一个是 Object 类型的 lock,用来当一个 Synchronized 的锁(我是JDK14版的,不同版本用的锁好像不一样),另一个是保存数据的 Object 类型的数组,名叫 array,它是 volatile 类型的,保证不同线程的可见性。
这里面注释也说了,源码的作者们当Java内置的 Synchronized 锁和 ReentrantLock 都可以用的时候,他们更偏爱 Synchronized 锁。
初始化
// 无参构造
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
// 用集合来构造
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] es;
if (c.getClass() == CopyOnWriteArrayList.class)
es = ((CopyOnWriteArrayList<?>)c).getArray();
else {
es = c.toArray();
if (c.getClass() != java.util.ArrayList.class)
es = Arrays.copyOf(es, es.length, Object[].class);
}
setArray(es);
}
// 用数组来构造
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
// setArray 方法
final void setArray(Object[] a) {
array = a;
}
// getArray 方法
final Object[] getArray() {
return array;
}
复制代码
三种初始化方法:
- 无参的最简单,不用说;
- 用集合来构造:如果此集合也为 CopyOnWriteArrayList,那直接让 es 指向对方 array 指向的数组,否则,如果是 ArrayList,运行
es = c.toArray();
,如果不是 ArrayList,执行es = Arrays.copyOf(es, es.length, Object[].class);
,有人问了,这数组本来就是 es,再调用Arrays.copyOf(es, es.length, Object[].class)
有啥意义?这个三参数版本的 Arrays.copyOf 的作用就是得到一个新数组,数组的元素由 es 复制而来,长度为 es.length,但是数组类型得为 Object 类型,所以还是有意义的,有些集合它 toArray 方法可能返回的不是 Object 类型的数组。这个我在 ArrayList 的博客里也说过。 - 用数组来构造:三参数的 Arrays.copyOf 方法上面已经说过了,不解释。
写操作
首先看看写操作,在 CopyOnWriteArrayList 里,只要对原数组有一点改动,都是写操作,都得加锁,哪怕只是 set 方法。(回想 ArrayList,它有个 modCount属性,标识数组有没有发生结构性改变,但是 ArrayList 的 set 方法却不会改变 modCount,所以 CopyOnWriteArrayList 对写真的很严格。)
set 方法
public E set(int index, E element) {
// 抢锁
synchronized (lock) {
Object[] es = getArray();
E oldValue = elementAt(es, index);
// 确实需要修改
if (oldValue != element) {
// 复制一份
es = es.clone();
// 修改
es[index] = element;
}
setArray(es);
return oldValue;
}
}
复制代码
可以看到,set 需要抢锁,毕竟是线程安全的集合,这样我觉得也是合理的。如果确实需要修改,那么es = es.clone();
,让 es 指向克隆的数组,虽然名字还是 es,但现在已经有两份数组了,es 指向新的那份,接着进行修改。最后运行setArray(es);
,让 array 指向这份新的数组。
不难看出,仅仅是一个简单的 set 操作,却要复制整个数组,如果数组很大,效率会比较一般。
add 操作
add 一个元素
这类方法有好几个版本,先看最简单的两个:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
// 克隆一个新数组,长度 + 1
es = Arrays.copyOf(es, len + 1);
// 尾部赋值
es[len] = e;
setArray(es);
return true;
}
}
public void add(int index, E element) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
Object[] newElements;
// 需要挪动的元素个数
int numMoved = len - index;
// 不用挪,直接尾部赋值
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, newElements, index + 1,
numMoved);
}
// 赋值
newElements[index] = element;
setArray(newElements);
}
}
复制代码
第一个 add 方法是在尾部追加一个元素,直接克隆了一个长度 + 1 的数组,并进行赋值;第二个 add 方法其实和 ArrayList 的 add 差不多,也是先看有多少元素要往后移,接着进行相应的移动,然后在 index 处赋值。但是还是有不同的:
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
复制代码
ArrayList 只会把 index 之后的元素后移,之前的不管。CopyOnWriteArrayList 却都要操作,这也很好理解,毕竟 CopyOnWriteArrayList 声明了一个新数组嘛,当然所有元素都要复制了。
也可以看出来,CopyOnWriteArrayList 是不需要扩容的,为啥?ArrayList 需要扩容是因为起码在下次扩容之前,add 操作还是在 elemantData 这个数组上赋值的,它不会每次 add 都弄一个新数组出来,那么把容量多扩一点,也省得频繁扩容,频繁线性时间复杂度复制元素了;而 CopyOnWriteArrayList 每次 add 都新复制一个数组,有什么扩容的必要呢,即使扩了,下次还是复制一个新的出来,没有意义。所以 CopyOnWriteArrayList 每次 add 都弄了一个原长度 + 1 的新数组。
接下来看难一点的 addIfAbsent:
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
// 运用逻辑表达式短路性质
return indexOfRange(e, snapshot, 0, snapshot.length) < 0
&& addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
synchronized (lock) {
// 真正的添加元素,一堆代码,之后再细看
}
}
复制代码
上来是一个逻辑表达式的短路操作,先调用indexOfRange(e, snapshot, 0, snapshot.length)
,这个函数很简单:
private static int indexOfRange(Object o, Object[] es, int from, int to) {
if (o == null) {
for (int i = from; i < to; i++)
if (es[i] == null)
return i;
} else {
for (int i = from; i < to; i++)
if (o.equals(es[i]))
return i;
}
return -1;
}
复制代码
没什么可解释的,从头到尾找元素!
由于是 addIfAbsent,所以当indexOfRange(e, snapshot, 0, snapshot.length) < 0 && addIfAbsent(e, snapshot);
前部分为真时,也就是确实没这个元素,才会进一步调用真正的 add,否则根本不会往下走了。这种短路操作在 Java 源码里太常见了。。。接下来看真正的 add:
private boolean addIfAbsent(E e, Object[] snapshot) {
synchronized (lock) {
// 新的 array 指向的数组
Object[] current = getArray();
int len = current.length;
// 数组发生了改变,要重新判断
if (snapshot != current) {
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i]
&& Objects.equals(e, current[i]))
// 这个元素在 current 前半部分里出现了,返回false
return false;
// // 这个元素在 current 后半部分里出现了,返回false
if (indexOfRange(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
复制代码
那既然这个元素在数组里没有,那直接追加在尾部不就行了,为啥又要这一堆代码呢?那是因为一开始是在Object[] snapshot = getArray();
的 snapshot 里进行找的,但这只是个快照,这个快照的内容在生成的一瞬间就确定了,不会改变。从上面一些写操作的分析,咱们也知道,CopyOnWriteArrayList 只要有写操作,就会改变数组的引用。所以可能你对 snapshot 进行 indexOfRange 的时间有点长,虽然你最后在 snopshot 里确实没找到这个元素,但可能在这段时间里,其它写操作改了 CopyOnWriteArrayList 的数组引用,当你找完 snopshot 并发元素不存在时,其它操作让 CopyOnWriteArrayList 的引用数组里有了这个元素,那么你再往里追加,不就错了嘛;或者你找完的时候,其它线程正在进行写操作,你走到这第一行代码synchronized (lock) {
,连锁都抢不到,然后一直抢,等你抢到的时候,数组都不知道变成什么样了,,,是这个意思。
解释了为啥有这一堆代码,咱们来仔细看看这个代码。取到了现在 CopyOnWriteArrayList 的快照 current 和它的长度 len。然后判断if (snapshot != current)
,如果两次快照一样,那太好了,不用担心我上面说的情况了,直接追加元素吧!
否则,进入到 if 代码块里。int common = Math.min(snapshot.length, len);
得到两个快照的最小长度,接着for (int i = 0; i < common; i++)
循环遍历查找。这里查找的代码也很有意思:
if (current[i] != snapshot[i]
&& Objects.equals(e, current[i]))
// 这个元素在 current 前半部分里出现了,返回false
return false;
复制代码
这又是个短路操作。先看逻辑表达式前半部分:current[i] != snapshot[i]
,咱们走到这个函数的前提是什么?是在 snopshot 里没找到 e 这个元素,那么如果current[i] == snapshot[i]
,不用比了,current[i] 和 e 肯定也不相同,根据逻辑表达式的短路性质,也不会往下走了;只有当current[i] != snapshot[i]
为真,才有把 current[i] 和 e 比较的必要,接下来进行Objects.equals(e, current[i])
,如果这个也真,那没办法,看来新的快照确实有 e 这个元素,返回 false 吧 (T_T) 。至于为啥不直接比 current[i] 和 e,猜想可能是==
操作比Objects.equals()
更快吧。
上面说的这个操作完了之后,还有if (indexOfRange(e, current, common, len) >= 0)
这个判断,毕竟刚刚为防数组溢出,只比了int common = Math.min(snapshot.length, len);
这么多元素嘛,可能还没比完呢。
总之,比完了之后,如果在新快照里仍然没这个元素,那就追加此元素!
有人问了,那我在 current 这个快照找的时候,期间就不会有别的写操作改了数组的引用吗?答案是不会,注意到private boolean addIfAbsent
这个函数进来就是个抢锁的操作,所以倘若你得到了 current 这个快照,那么直到你离开这段代码前,都不会有别的线程可以进行写操作,改数组的引用。
add 一堆元素
先看简单版本的:
public boolean addAll(Collection<? extends E> c) {
// 取到对应的元素们
Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
// 没什么好添加的
if (cs.length == 0)
return false;
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
Object[] newElements;
// 如果原数组长度为0,且集合是 ArrayList 或 CopyOnWriteArrayList,直接赋值
if (len == 0 && (c.getClass() == CopyOnWriteArrayList.class ||
c.getClass() == ArrayList.class)) {
newElements = cs;
// 否则,在尾部追加这些元素
} else {
newElements = Arrays.copyOf(es, len + cs.length);
System.arraycopy(cs, 0, newElements, len, cs.length);
}
setArray(newElements);
return true;
}
}
复制代码
这个代码也比较简单,就是经过一些判断之后(比方说待添加集合为空,直接返回 false),在原数组尾部追加集合里的元素,注释里写的很清楚了,不解释了。这个函数还有个这个版本的:public boolean addAll(int index, Collection<? extends E> c)
,从特定 index 后面添加集合里的元素,基本思想都一样,就是元素的挪来挪去,不想赘述了,大家自己找找看看,肯定能看懂。看下面这个难一点的:
public int addAllAbsent(Collection<? extends E> c) {
Object[] cs = c.toArray();
if (c.getClass() != ArrayList.class) {
cs = cs.clone();
}
// 集合为空,不需要添加,直接返回
if (cs.length == 0)
return 0;
synchronized (lock) {
// 一堆复杂代码,之后解释
}
}
复制代码
这个函数是如果数组里没有集合里的元素,才进行添加,有就不加了。然后先拿到这个集合的元素的数组。这块有这个判断:
if (c.getClass() != ArrayList.class) {
cs = cs.clone();
}
复制代码
为什么要这样,我也不太清楚,猜想可能是有些集合的 toArray 返回的是自己的数组本身,怕污染了集合的原数组?然后如果集合为空,那直接返回就行了,接下来看 synchronized 代码块里的代码,这是真正进行添加了:
synchronized (lock) {
// 拿到自己的快照,这之后不会有线程改变此快照
Object[] es = getArray();
int len = es.length;
int added = 0;
// 进行添加
for (int i = 0; i < cs.length; ++i) {
Object e = cs[i];
// e 不能在 CopyOnWriteArrayList 里出现
// 同时也不能添加重复的 e
if (indexOfRange(e, es, 0, len) < 0 &&
indexOfRange(e, cs, 0, added) < 0)
cs[added++] = e;
}
// 有符合条件的元素需要添加
if (added > 0) {
// 尾部追加
Object[] newElements = Arrays.copyOf(es, len + added);
System.arraycopy(cs, 0, newElements, len, added);
setArray(newElements);
}
return added;
}
复制代码
这个代码主要看 for 循环里的代码,Object e = cs[i];
之后是一个 if,第一个条件indexOfRange(e, es, 0, len) < 0
很好理解,这个元素 e,我们的数组里确实不存在,那么第二个条件呢?indexOfRange(e, cs, 0, added) < 0
,注意到 added 初始化为 0,所以当第一个不存在于 CopyOnWriteArrayList 出现的时候,第二个条件肯定为真的,于是 added 变为了 1,那么再来第二个满足了第一个条件的 e 出现时,走一遍第二个条件,意思就是这个 e 不仅不能在 CopyOnWriteArrayList 里出现,也不能在cs[0:added]
里出现,就是不能重复添加元素。这也符合addAllAbsent
的语义,同样的元素如果添加了两次,和这个 Absent 可能有点冲突吧。
最后就是在尾部追加cs[0:added]
的元素了。
remove 操作
remove 一个元素
最简单的按下标删:
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;
}
}
复制代码
就是先int numMoved = len - index - 1;
计算有多少元素要往前挪,如果没有元素往前挪,说明删的最后一个元素,直接newElements = Arrays.copyOf(es, len - 1);
,否则,先把 0 - index 处的元素复制到 newElements,再把 index 后的元素往前挪动一步,复制到 newElements 里。
再看按元素删:
// 先找一下元素有没有
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOfRange(o, snapshot, 0, snapshot.length);
// 在快照里找到了,再真正 remove
return index >= 0 && remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
synchronized (lock) {
// 一堆操作,之后细说
}
}
复制代码
和 addIfAbsent 一样,同样是先在快照里找,找到了才进行下一步,return index >= 0 && remove(o, snapshot, index);
,仍然是短路操作。接下来看 synchronized 代码块的内容:
synchronized (lock) {
// 拿到目前的快照
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]
&& Objects.equals(o, current[i])) {
index = i;
break findIndex;
}
}
// 一些边界判断
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
// 后半部分找(如果有的话)
index = indexOfRange(o, current, index, len);
if (index < 0)
return false;
}
// 删除 index 处的元素的常规操作
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;
}
复制代码
if (snapshot != current)
如果为假,那直接进行删除就行了。
否则,需要走 if 里那一堆代码。和 addIfAbsent 一样的思想,先int prefix = Math.min(index, len);
,从 0 到 prefix 找,因为 prefix 肯定小于等于 index,而 index 又是上一个快照第一次找到 o 的索引,所以 0 到 prefix 的寻找可以用if (current[i] != snapshot[i] && Objects.equals(o, current[i])) {
来优化了。只有current[i] != snapshot[i]
时,才有进一步Objects.equals(o, current[i])
的必要。如果找到了满足Objects.equals(o, current[i])
的 i,将它赋值给 index,然后 break 跳出来;
如果 0 到 prefix 找完了也没找到,出来这个 for 循环之后,如果 index >= len,说明 0 到 len 都没有找到,返回 false;否则,index < len,还有 index 到 len 没有找,先if (current[index] == o)
,进行一个快速匹配,如果这个找到了,直接 break 跳出来,否则,老老实实index = indexOfRange(o, current, index, len);
在后半部分找,如果后半部分也没找到,即if (index < 0)
,那返回 false。
总之,最后找到了,再运行那一段删除 index 处元素的代码即可。
remove 一堆元素
首先还是看简单的版本:
void removeRange(int fromIndex, int toIndex) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
// 下标检查
if (fromIndex < 0 || toIndex > len || toIndex < fromIndex)
throw new IndexOutOfBoundsException();
// 新长度
int newlen = len - (toIndex - fromIndex);
// 多少元素往前挪
int numMoved = len - toIndex;
// 没有元素需要挪
if (numMoved == 0)
setArray(Arrays.copyOf(es, newlen));
// 进行一系列挪动
else {
Object[] newElements = new Object[newlen];
System.arraycopy(es, 0, newElements, 0, fromIndex);
System.arraycopy(es, toIndex, newElements,
fromIndex, numMoved);
setArray(newElements);
}
}
}
复制代码
这个代码先检查 fromIndex 和 toIndex 的下标是否合法,然后计算新长度和有多少元素需要前挪,如果没有元素往前挪,直接setArray(Arrays.copyOf(es, newlen));
即可,否则,先复制 0 到 fromIndex 的元素,再复制 toIndex 到 len 的元素。反正就是挪动 + 复制。再看难一点的:
// 删除位于集合 c 里的数组里的元素
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return bulkRemove(e -> c.contains(e));
}
// 保留位于集合 c 里的数组里的元素,删除集合 c 里没有的元素
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return bulkRemove(e -> !c.contains(e));
}
private boolean bulkRemove(Predicate<? super E> filter) {
synchronized (lock) {
return bulkRemove(filter, 0, getArray().length);
}
}
复制代码
这里用 lambda 表达式表示了删除的条件,bulkRemove 里是一个 Predicate 的函数式接口。看一下这个三参数版的 bulkRemove。
算了,这个代码真的很长,我就不复制了,这是一个用 long 类型的数组和位操作巧妙的进行删除的方法,不是很容易看懂,之前我的这篇博客很详细的介绍了这种方法:ArrayList源码学习(三):removeIf,我就不在这复制黏贴了,反正这个思想挺有意思的。
写操作到这就告一段落吧,还有什么 replace 方法,都太简单了,没什么说的意义。总结下写操作就是:只要改了原数组了,必定会触发数组的所有元素的复制,以及 setArray 方法,哪怕是最简单的 set 方法。
读操作
读操作就简单很多了,这里放上几个常见的读操作代码:
// get 方法
public E get(int index) {
return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
return (E) a[index];
}
// 根据元素找下标
public int indexOf(Object o) {
Object[] es = getArray();
return indexOfRange(o, es, 0, es.length);
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
if (o == null) {
for (int i = from; i < to; i++)
if (es[i] == null)
return i;
} else {
for (int i = from; i < to; i++)
if (o.equals(es[i]))
return i;
}
return -1;
}
复制代码
可以看到,读操作不需要加锁,也不用复制数组元素,读上来会调用个 getArray 方法,拿到这个快照,对这个快照进行查找。
CopyOnWriteArrayList 读写小结
看完了大部分的 CopyOnWriteArrayList 的读写操作的代码,咱们可以总结下它的读写的特点:
- 写操作要加锁,抢锁,同时只有一个线程能写,并且写的时候要复制整个数组的元素
- 读操作不用加锁,会调用 getArray 方法得到快照,对此快照进行查找
- 写写互斥(因为要抢锁),读读不互斥,读写不互斥
- 读操作得到的结果可能不是最新的,因为只是个快照的内容。读操作可以得到的结果在快照生成那一瞬间就确定了
- 写操作时,并不会直接对现有的数组/快照进行修改,所以读操作不用担心读着读着,快照被其它写操作改了
- 写完之后,会 setArray 改变当前 CopyOnWriteArrayList 的 array 数组
迭代器
初始化和基本结构
这是几个调迭代器的函数,都是传给 COWIterator 方法一个 getArray 得到的快照,和开始的 index。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
public ListIterator<E> listIterator() {
return new COWIterator<E>(getArray(), 0);
}
public ListIterator<E> listIterator(int index) {
Object[] es = getArray();
int len = es.length;
if (index < 0 || index > len)
throw new IndexOutOfBoundsException(outOfBounds(index, len));
return new COWIterator<E>(es, index);
}
复制代码
下面看看它的基本结构以及初始化:
static final class COWIterator<E> implements ListIterator<E> {
// 迭代器对应的快照
private final Object[] snapshot;
// 将访问的数组索引
private int cursor;
// 初始化
COWIterator(Object[] es, int initialCursor) {
cursor = initialCursor;
snapshot = es;
}
// 其它操作代码
}
复制代码
不难看出,迭代器本身维护一个 CopyOnWriteArrayList 的快照 snapshot,以及 cursor 作为下一个要访问的数组索引。
next 和 previous 方法
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
@SuppressWarnings("unchecked")
public E previous() {
if (! hasPrevious())
throw new NoSuchElementException();
return (E) snapshot[--cursor];
}
复制代码
反正每次调用前都看看if (! hasNext())
或者if (! hasPrevious())
,代码层面实在是很简单,不说了。
写操作
public void remove() {
throw new UnsupportedOperationException();
}
public void set(E e) {
throw new UnsupportedOperationException();
}
public void add(E e) {
throw new UnsupportedOperationException();
}
复制代码
看来作者 Doug Lea
大佬不愿意让 CopyOnWriteArrayList 的迭代器有修改的功能。。。
迭代器小结
迭代器也是通过拿到 CopyOnWriteArrayList 某一瞬间的快照,对此快照进行操作。
写时复制思想
Linux 中的写时复制
当初上操作系统课时,记得老师布置过这种父子进程创建和通信的作业。在Linux程序中,fork()会产生一个和父进程完全相同的子进程,出于效率考虑,Linux 没有逐个复制父进程的数据给子进程,而是引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。
写时复制思想的应用
Redis 的 RDB 持久化
以我目前的水平来说,除了 Java 这个 CopyOnWriteArrayList 和 CopyOnWriteArraySet(基于CopyOnWriteArrayList,代码很少),这块应用还知道的就是 Redis 的 RDB 持久化了。
RDB 持久化有两种,一种调用 save,由主进程执行持久化,会导致单进程的 Redis 阻塞很久,一般不推荐;另一种就是 调用 bgsave,fork 一个子进程来进行持久化,由于 RDB 持久化属于只读操作,不会修改原数据,所以也比较适用写时复制思想。RDB 可以得到的快照在子进程创建出来的瞬间就确定了。
总结
本文主要对 CopyOnWriteArrayList 的主要源码进行了解析,由于工作量比较大,有些源码如 subList 没有讲,大家感兴趣可以自己去看看,比较简单。