0
点赞
收藏
分享

微信扫一扫

Java并发编程——CopyOnWriteArrayList详解

一、 CopyOnWriteArrayList介绍

  • CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
  • CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差
  • 在迭代器上进行的元素更改操作(remove、set和add)不受支持。这些方法将抛出UnsupportedOperationException。

它相当于线程安全的ArrayList。和ArrayList一样,它是个可变数组;但是和ArrayList不同的时,它具有以下特性:

  • 1.它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
  • 2.它是线程安全的。
  • 3.因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
  • 4.迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
  • 5.使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

image.png

二、 CopyOnWriteArrayList原理

  • 1.CopyOnWriteArrayList实现了List接口,因此它是一个队列。
  • 2.CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
  • 3.CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
  • 4.CopyOnWriteArrayList的“动态数组”机制 -- 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很 低;但是单单只是进行遍历查找的话,效率比较高。
  • 5.CopyOnWriteArrayList的“线程安全”机制 -- 是通过volatile和监视器锁Synchrnoized来实现的。
  • 6.CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的 保证。
  • 7.CopyOnWriteArrayList通过监视器锁Synchrnoized来保护数据。在“添加/修改/删除”数据时,会先“获取监视器锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。

三、 CopyOnWriteArrayList 属性介绍

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

    /**
     * 监视器锁
     */
    final transient Object lock = new Object();

    /** 一个缓存数组,使用volatile修饰。 */
    private transient volatile Object[] array;

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }
}

四、 构造器

  • 无参构造,默认创建的是一个长度为0的数组
//无参构造,默认创建的是一个长度为0的数组
public CopyOnWriteArrayList() {
	setArray(new Object[0]);
}
  • 参数为Collection的构造方法
//参数为Collection的构造方法
public CopyOnWriteArrayList(Collection<? extends E> c) {
	Object[] es;
	//判断传递的是否就是一个CopyOnWriteArrayList集合
	if (c.getClass() == CopyOnWriteArrayList.class)
		//如果是,直接调用getArray方法,获得传入集合的array然后赋值给elements
		es = ((CopyOnWriteArrayList<?>)c).getArray();
	else {
		//先将传入的集合转变为数组形式
		es = c.toArray();
		//c.toArray()可能不会正确地返回一个 Object[]数组,那么使用Arrays.copyOf()方法
		if (c.getClass() != java.util.ArrayList.class)
			es = Arrays.copyOf(es, es.length, Object[].class);
	}
	//直接调用setArray方法设置array属性
	setArray(es);
}
  • 创建一个包含给定数组副本的list
//创建一个包含给定数组副本的list
public CopyOnWriteArrayList(E[] toCopyIn) {
	setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

说明:

  • setArray()的作用是给array赋值;其中,array是volatile transient Object[]类型,即array是“volatile数组”。
  • 关于volatile关键字,我们知道“volatile能让变量变得可见”,即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。正在由于这种特性,每次更新了“volatile数组”之后,其它线程都能看到对它所做的更新。
  • 关于transient关键字,它是在序列化中才起作用,transient变量不会被自动序列化。

上面介绍的是CopyOnWriteList的初始化,三个构造方法都比较易懂

五、添加add方法

public boolean add(E e) {
	//加锁
	synchronized (lock) {
		//获得list底层的数组array
		Object[] es = getArray();
		//获得数组长度
		int len = es.length;
		//拷贝到新数组,新数组长度为len+1
		es = Arrays.copyOf(es, len + 1);
		//给新数组末尾元素赋值
		es[len] = e;
		//用新的数组替换掉原来的数组
		setArray(es);
		return true;
	}
}
  • 1、加锁(synchronized )。保证此时只有一个线程进入
  • 2、获取原来的数组以及长度
  • 3、复制新数据,并且长度+1
  • 4、设置新数据(array使用volatile修饰,所以其他线程可见)
  • 5、返回

六、获取元素 get(int index)

使用get(i)可以获取指定位置i的元素,当然如果元素不存在就会抛出数组越界异常。

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

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

六、remove(int index)

public E remove(int index) {
	//加锁
	synchronized (lock) {
		//获取原数组
		Object[] es = getArray();
		//获取原数组长度
		int len = es.length;
		//获取原数组index处的值
		E oldValue = elementAt(es, index);
		//因为数组删除元素需要移动,所以这里就是计算需要移动的个数
		int numMoved = len - index - 1;
		Object[] newElements;
		if (numMoved == 0)
			//计算的numMoved=0,表示要删除的是最后一个元素
			//那么旧直接将原数组的前len-1个复制到新数组中,替换旧数组即可
			newElements = Arrays.copyOf(es, len - 1);
			
		//要删除的不是最后一个元素	
		else {
			//创建一个长度为len-1的数组
			newElements = new Object[len - 1];
			//将原数组中index之前的元素复制到新数组
			System.arraycopy(es, 0, newElements, 0, index);
			//将原数组中index之后的元素复制到新数组
			System.arraycopy(es, index + 1, newElements, index,
							 numMoved);
		}
		//用新数组替换原数组
		setArray(newElements);
		return oldValue;
	}
}

remove(int index)的作用就是将”volatile数组“中第index个元素删除。它的实现方式是,如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组。否则,新建数组,然后将”volatile数组中被删除元素之外的其它元素“拷贝到新数组中;最后,将新数组赋值给”volatile数组“。和add(E e)一样,remove(int index)也是”在操作之前,获取独占锁;操作完成之后,释放独占是“;并且”在操作完成时,会通过将数据更新到volatile数组中“。

七、修改元素

修改也是属于写 ,所以需要获取lock,下面就是set方法的实现

public E set(int index, E element) {
	//加锁
	synchronized (lock) {
		//获取数组array
		Object[] es = getArray();
		//获取index位置的元素
		E oldValue = elementAt(es, index);
		// 要修改的值和原值不相等
		if (oldValue != element) {
			//克隆一个新数组
			es = es.clone();
			//替换元素
			es[index] = element;
		}
		// Ensure volatile write semantics even when oldvalue == element
		// 即使在oldvalue==元素的情况下也要确保易失性写入语义
		setArray(es);
		return oldValue;
	}
}

八、 迭代器

public Iterator<E> iterator() {
	return new COWIterator<E>(getArray(), 0);
}
  • 使用了COWIterator遍历
static final class COWIterator<E> implements ListIterator<E> {
	/** Snapshot of the array */
	//array的快照版本
	private final Object[] snapshot;
	/** Index of element to be returned by subsequent call to next.  */
	//后续调用next返回的元素索引(数组下标)
	private int cursor;

	//构造器
	COWIterator(Object[] es, int initialCursor) {
		cursor = initialCursor;
		snapshot = es;
	}
	
	//变量是否结束:下标小于数组长度
	public boolean hasNext() {
		return cursor < snapshot.length;
	}

	//是否有前驱元素
	public boolean hasPrevious() {
		return cursor > 0;
	}

	//获取元素
	//hasNext()返回true,直接通过cursor记录的下标获取值
	//hasNext()返回false,抛出异常
	@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];
	}

	public int nextIndex() {
		return cursor;
	}

	public int previousIndex() {
		return cursor - 1;
	}

	/**
	 * Not supported. Always throws UnsupportedOperationException.
	 * @throws UnsupportedOperationException always; {@code remove}
	 *         is not supported by this iterator.
	 */
	public void remove() {
		throw new UnsupportedOperationException();
	}

	/**
	 * Not supported. Always throws UnsupportedOperationException.
	 * @throws UnsupportedOperationException always; {@code set}
	 *         is not supported by this iterator.
	 */
	public void set(E e) {
		throw new UnsupportedOperationException();
	}

	/**
	 * Not supported. Always throws UnsupportedOperationException.
	 * @throws UnsupportedOperationException always; {@code add}
	 *         is not supported by this iterator.
	 */
	public void add(E e) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void forEachRemaining(Consumer<? super E> action) {
		Objects.requireNonNull(action);
		final int size = snapshot.length;
		int i = cursor;
		cursor = size;
		for (; i < size; i++)
			action.accept(elementAt(snapshot, i));
	}
}

说明:COWIterator不支持修改元素的操作。例如,对于remove(),set(),add()等操作,COWIterator都会抛出异常!

 

另外,需要提到的一点是,CopyOnWriteArrayList返回迭代器不会抛出ConcurrentModificationException异常,即它不是fail-fast机制的!

 

参考: https://blog.csdn.net/GoSaint/article/details/115234349

https://www.cnblogs.com/zhangboyu/p/7452542.html

https://www.cnblogs.com/-beyond/p/13130040.html

举报

相关推荐

0 条评论