三十八、并发集合之CopyOnWriteArrayList

49 阅读9分钟

CopyOnWriteArrayList

CopyOnWriteArrayList概述

CopyOnWriteArrayList是一个线程安全的ArrayList

CopyOnWriteArrayList是基于Lock锁和数组副本的形式保证线程安全

在操作数据时,需要先获取lock锁,然后复制一个副本数组,将数据插入副本数组中,将副本数组赋值给array属性变量

因为CopyOnWriteArrayList每次写数据都会构建副本数组,如果业务是写多,并且数组中数据量较大,尽量避免使用CopyOnWriteArrayList,因为比较占用内存资源

CopyOnWriteArrayList是弱一致性的。写操作先执行,但是副本数组还没有赋值给array属性变量,此时读操作读取到的是旧数组中的值

CopyOnWriteArrayList的核心属性

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    // 互斥锁
    final transient ReentrantLock lock = new ReentrantLock();

    // 存放数据的数组
    private transient volatile Object[] array;

    // 获取array属性的方法
    final Object[] getArray() {
        return array;
    }

    // 设置array属性的方法
    final void setArray(Object[] a) {
        array = a;
    }

    // CopyOnWriteArrayList的无参构造方法
	// 默认创建的CopyOnWriteArrayList的长度是0
	// 每次添加数据都会构建新的数组
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
}

CopyOnWriteArrayList的基本方法

get方法

public E get(int index) {
	return get(getArray(), index);
}

private E get(Object[] a, int index) {
	return (E) a[index];
}

获取数组的副本,然后根据索引查询副本中的数据。

add方法

add方法-不指定索引

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;
		// array属性赋值为新数组
		setArray(newElements);
		// 添加完成,返回true
		return true;
	} finally {
		// 释放锁
		lock.unlock();
	}
}

步骤:

  1. 加锁
  2. 获取数组副本和数组长度
  3. 创建新数组,新数组长度比旧数组多一位,并且将旧数组数据迁移到新数组中,位置不变
  4. 将新元素添加到新数组的尾部
  5. 释放锁并且返回true

add方法-指定索引

// 此方法不会覆盖数组中索引上的数据
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];
			// 将旧数组元素添加到新数组中
			// 但是索引位置空出来
			System.arraycopy(elements, 0, newElements, 0, index);
			System.arraycopy(elements, index, newElements, index + 1,
							 numMoved);
		}
		// 将新元素添加到新数组的索引位置
		newElements[index] = element;
		// array属性赋值为新数组
		setArray(newElements);
	} finally {
		// 释放锁
		lock.unlock();
	}
}

步骤:

  1. 加锁
  2. 获取数组副本和数组长度
  3. 判断索引是否超出数组长度,超出抛异常
  4. 判断索引是否等于数组长度,如果等于,创建新数组,长度比旧数组多一位,将新元素放到新数组的尾部
  5. 如果索引在数组中间某个位置,创建新数组,长度比旧数组多一位,将旧数组中从0开始到指定索引位之前的数据迁移到新数组中,位置不变;将旧数组中指定索引位到最后一位的数据迁移到新数组中,位置往后移一位,为新元素腾出位置。最后将新元素放到新数组的指定索引位置
  6. 释放锁

remove方法

remove方法-指定索引

public 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)
			// 进入这里说明删除的是数组尾部的数据
			// 构建一个长度减1的新数组,并且赋值给array属性
			setArray(Arrays.copyOf(elements, len - 1));
		else {
			// 进入这里说明删除的是数组中间的数据
			// 构建一个长度减1的新数组
			Object[] newElements = new Object[len - 1];
			// 将旧数组中的数据赋值给新数组,不包括索引位置的数据
			System.arraycopy(elements, 0, newElements, 0, index);
			System.arraycopy(elements, index + 1, newElements, index,
							 numMoved);
			// array属性赋值新数组
			setArray(newElements);
		}
		// 返回旧数据
		return oldValue;
	} finally {
		// 释放锁
		lock.unlock();
	}
}

步骤:

  1. 加锁
  2. 获取数组副本以及数组长度
  3. 判断索引是否是数组最后一位,如果是,创建长度减1的新数组,将旧数组数据迁移到新数据中,丢弃最后一位数据
  4. 如果不是,创建长度减1的新数组,将旧数组从0到指定索引位置前的元素以及指定索引位置后面的元素迁移到新数组中,丢弃指定索引位置的元素
  5. 释放锁,返回指定索引位置的数据

remove方法-指定数据

public boolean remove(Object o) {
	// 获取数组
	Object[] snapshot = getArray();
	// 计算出数据在数组中的索引位置
	// 因为CopyOnWriteArrayList中可以存放相同的数据
	// 这里只会返回较小的索引数据的位置
	// 所以这个remove方法优先删除的是索引较小位置的数据
	// 如果CopyOnWriteArrayList中没有这个数据,返回-1
	int index = indexOf(o, snapshot, 0, snapshot.length);
	// 如果CopyOnWriteArrayList中没有这个数据,返回false
	// 否则执行remove重载方法
	return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
	final ReentrantLock lock = this.lock;
	// 加锁
	lock.lock();
	try {
		Object[] current = getArray();
		int len = current.length;
		// 判断当前数组是否发生变化
		// 因为之前获取索引时未加锁,所以如果数组变化了,需要重新获取索引
		// if判断语句后面的findIndex是一个标识
		// if代码块中如果使用break findIndex,后面代码不会执行,直接跳出if代码块
		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])) {
					// 在当前数组中找到了要删除元素的索引位置
					index = i;
					// 跳出if代码块
					break findIndex;
				}
			}
			// for循环执行完,还没有跳出if代码块,说明没有找到删除的元素
			// 判断当前数组长度是否小于索引大小
			// 如果是,说明当前数组中已经没有这个元素了
			if (index >= len)
				// 返回false
				return false;
			// 判断当前数组的索引位置是否就是要删除的元素
			// 如果是,说明元素位置没有改变
			if (current[index] == o)
				// 跳出if代码块
				break findIndex;
			// 最后一招,遍历当前数组
			index = indexOf(o, current, index, len);
			// 如果index小于0,说明当前数组中没有要删除的元素
			if (index < 0)
				// 返回false
				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();
	}
}

步骤:

  1. 获取数组的副本
  2. 获取元素的索引,索引小于0,直接返回false
  3. 加锁
  4. 再次获取数组副本,判断数组是否发生变化
  5. 如果发生变化,重新获取元素的索引
  6. 如果数组中没有这个元素了,直接返回false
  7. 如果数组中有这个元素,创建长度减1的新数组,将旧数组中的元素迁移到新数组中,除了指定索引位置的元素
  8. 释放锁

set方法

public 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 {
			// 旧数据与新数据一样,不做操作
			setArray(elements);
		}
		return oldValue;
	} finally {
		lock.unlock();
	}
}

步骤:

  1. 加锁
  2. 获取数组的副本和指定索引位置的数据
  3. 判断新元素与旧元素的值是否相等,如果相等,什么也不做;如果不相等,创建新数组,将旧数组元素迁移到新数组中,并且将指定索引位置赋值为新元素
  4. 释放锁,返回旧元素

clear方法

public void clear() {
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		setArray(new Object[0]);
	} finally {
		lock.unlock();
	}
}

步骤:

  1. 加锁
  2. 创建长度为0的新数组,并且赋值给array
  3. 释放锁

迭代器

public Iterator<E> iterator() {
	// 返回COWIterator对象
	// 默认从索引0位置开始遍历
	return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
	// 遍历的数组副本
	private final Object[] snapshot;

	// 遍历用的游标
	private int cursor;

	private COWIterator(Object[] elements, int initialCursor) {
		cursor = initialCursor;
		snapshot = elements;
	}

	// 判断是否有下一个值
	public boolean hasNext() {
		// 游标小于数组长度,说明有下一个值
		return cursor < snapshot.length;
	}

	// 判断是否有上一个值
	public boolean hasPrevious() {
		// 游标大于0,说明有上一个值
		return cursor > 0;
	}

	// 获取下一个值
	public E next() {
		if (! hasNext())
			// 没有下一个值,抛出异常
			throw new NoSuchElementException();
		// 先获取游标索引位置的值,再将游标加1
		return (E) snapshot[cursor++];
	}

	// 获取上一个值
	public E previous() {
		if (! hasPrevious())
			throw new NoSuchElementException();
		// 先将游标减1,在获取游标索引位置的值
		return (E) snapshot[--cursor];
	}

	// 获取下一个值的索引
	public int nextIndex() {
		return cursor;
	}

	// 获取上一个值的索引
	public int previousIndex() {
		return cursor-1;
	}

	// 下面的增删改操作都不允许做
	public void remove() {
		throw new UnsupportedOperationException();
	}

	public void set(E e) {
		throw new UnsupportedOperationException();
	}

	public void add(E e) {
		throw new UnsupportedOperationException();
	}

	// 这个方法是为了兼容lambda表达式,支持函数式编程
	@Override
	public void forEachRemaining(Consumer<? super E> action) {
		Objects.requireNonNull(action);
		Object[] elements = snapshot;
		final int size = elements.length;
		for (int i = cursor; i < size; i++) {
			@SuppressWarnings("unchecked") E e = (E) elements[i];
			action.accept(e);
		}
		cursor = size;
	}
}