[java源码解析]CopyOnWriteArrayList

本篇文章来源于慕课网收费专栏,本文纯为自己保留,删减大量部分,查看原文请支持正版。

CopyOnWriteArrayList简介

在 ArrayList 的类注释上,JDK 就提醒了我们,如果要把 ArrayList 作为共享变量的话,是线程不安全的,推荐我们自己加锁或者使用 Collections.synchronizedList 方法,其实 JDK 还提供了另外一种线程安全的 List,叫做 CopyOnWriteArrayList,这个 List 具有以下特征

  1. 线程安全的,多线程环境下可以直接使用,无需加锁;
  2. 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;
  3. 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回
    去。

整体架构

从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:

  1. 加锁;
  2. 从原数组中拷贝出新数组;
  3. 在新数组上进行操作,并把新数组赋值给数组容器;
  4. 解锁。

除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到,代码如下:
在这里插入图片描述

类注释:

  1. 所有的操作都是线程安全的,因为操作都是在新拷贝数组上进行的;
  2. 数组的拷贝虽然有一定的成本,但往往比一般的替代方案效率高;
  3. 迭代过程中,不会影响到原来的数组,也不会抛出ConcurrentModificationException 异常

新增操作:

// 添加元素到数组尾部
public boolean add(E e) {
	final ReentrantLock lock = this.lock;
	// 加锁
	lock.lock();
	try {
	// 得到所有的原数组
	Object[] elements = getArray();
	int len = elements.length;
	// 拷贝到新数组里面,新数组的长度是 + 1 的,因为新增会多一个元素
	Object[] newElements = Arrays.copyOf(elements, len + 1);
	// 在新数组中进行赋值,新元素直接放在数组的尾部
	newElements[len] = e;
	// 替换掉原来的数组
	setArray(newElements);
	return true;
	// finally 里面释放锁,保证即使 try 发生了异常,仍然能够释放锁
	} finally {
	lock.unlock();
	}
}

从上面我们一可以看出整个add过程都是在持有锁的状态下进行的,通过加锁,来保证同一时刻只有一个线程能对同一个数组进行操作。

那么为什么加锁了还要进行数组的copy呢?

  1. volatile 关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。
  2. 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响。

指定位置添加元素的源码:

int numMoved = len - index;
// 如果要插入的位置正好等于数组的末尾,直接拷贝数组即可
if (numMoved == 0)
	newElements = Arrays.copyOf(elements, len + 1);
else {
	// 如果要插入的位置在数组的中间,就需要拷贝 2 次
	// 第一次从 0 拷贝到 index。
	// 第二次从 index+1 拷贝到末尾。
	newElements = new Object[len + 1];
	System.arraycopy(elements, 0, newElements, 0, index);
	System.arraycopy(elements, index, newElements, index + 1,
	numMoved);
}
// index 索引位置的值是空的,直接赋值即可。
newElements[index] = element;
// 把新数组的值赋值给数组的容器中
setArray(newElements);

小结:

从 add 系列方法可以看出,CopyOnWriteArrayList 通过加锁 + 数组拷贝+ volatile 来保证了线程安全,每一个要素都有着其独特的含义:

  1. 加锁:保证同一时刻数组只能被一个线程操作;
  2. 数组拷贝:保证数组的内存地址被修改,修改后触发 volatile 的可见性,其它线程可以立马
    知道数组已经被修改;
  3. volatile:值被修改后,其它线程能够立马感知最新值。

批量删除

// 批量删除包含在 c 中的元素
public 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;
		// 说明数组有值,数组无值直接返回 false
		if (len != 0) {
			// newlen 表示新数组的索引位置,新数组中存在不包含在 c 中的元素
			int newlen = 0;
			Object[] temp = new Object[len];
			// 循环,把不包含在 c 里面的元素,放到新数组中
			for (int i = 0; i < len; ++i) {
			Object element = elements[i];
		// 不包含在 c 中的元素,从 0 开始放到新数组中
	if (!c.contains(element))
			temp[newlen++] = element;
	}
	// 拷贝新数组,变相的删除了不包含在 c 中的元素
	if (newlen != len) {
		setArray(Arrays.copyOf(temp, newlen));
		return true;
	}
	}
		return false;
	} finally {
		lock.unlock();
	}

从源码中,我们可以看到,我们并不会直接对数组中的元素进行挨个删除,而是先对数组中的值进行循环判断,把我们不需要删除的数据放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。

不知道大家有木有似曾相识的感觉,ArrayList 的批量删除的思想也是和这个类似的,所以我们在需要删除多个元素的时候,最好都使用这种批量删除的思想,而不是采用在 for 循环中使用单个删除的方法,单个删除的话,在每次删除的时候都会进行一次数组拷贝(删除最后一个元素时不会拷贝),很消耗性能,也耗时,会导致加锁时间太长,并发大的情况下,会造成大量请求在等待锁,这也会占用一定的内存。

迭代

在 CopyOnWriteArrayList 类注释中,明确说明了,在其迭代过程中,即使数组的原值被改变,也不会抛出 ConcurrentModificationException 异常,其根源在于数组的每次变动,都会生成新的数组,不会影响老数组,这样的话,迭代过程中,根本就不会发生迭代数组的变动,我们截几个图说明一下:

可以看到迭代器是直接持有原数组的引用:
[java源码解析]CopyOnWriteArrayList_第1张图片

  1. 我们写了一个 demo,在 CopyOnWriteArrayList 迭代之后,往 CopyOnWriteArrayList 里面新增值,从下图中可以看到在 CopyOnWriteArrayList 迭代之前,数组的内存地址是962,请记住这个数字:
    [java源码解析]CopyOnWriteArrayList_第2张图片
  2. CopyOnWriteArrayList 迭代之后,我们使用 add(“50”) 代码给数组新增一个数据后,数
    组内存地址发生了变化,内存地址从原来的 962 变成了 968,这是因为
    CopyOnWriteArrayList 的 add 操作,会生成新的数组,所以数组的内存地址发生了变化
    [java源码解析]CopyOnWriteArrayList_第3张图片
  3. 迭代继续进行时,我们发现迭代器中的地址仍然是迭代之前引用的地址,是 962,而不是新的数组的内存地址 [java源码解析]CopyOnWriteArrayList_第4张图片
    从上面 4 张截图,我们可以得到迭代过程中,即使 CopyOnWriteArrayList 的结构发生变动
    了,也不会抛出 ConcurrentModificationException 异常的原因:CopyOnWriteArrayList 迭代持有的是老数组的引用,而 CopyOnWriteArrayList 每次的数据变动,都会产生新的数组,
    对老数组的值不会产生影响,所以迭代也可以正常进行。

你可能感兴趣的:([java源码解析]CopyOnWriteArrayList)