关于 CopyOnWriteArrayList 的一个简单优化

一、优化动机

COW 简介:增删改都会加锁并拷贝工作数组,在拷贝数组上做完增删改操作后,会把拷贝数组切换为工作数组,在这个过程中,不会阻塞读操作。

所以 COW 很适合于读多写少的情况。

但是如果写再多一点呢,或者大部分的增删改只发生在工作数组的末尾呢。对于这些情况,在有大量数据的业务场景中,每次写操作都要拷贝整个工作数组,是很不划算的。

二、优化实现

考虑大部分的增删改只发生在工作数组末尾的这种情况。

我们可以使用一个数组来保存初始数据,然后后续发生在数组末尾的增删改数据保存在另一个数组中。

这样对于大量的增删改操作就只需要拷贝后面那个较短的数组即可,而不需要拷贝整个数据。

所以我考虑使用两个数组来保存数据,两个数组在逻辑上当成一个数组的前后两段使用。

三、优化导致的问题和解决方案

3.1 读的效率问题

COW 最显著的优点,就是读很快,任何写操作都不会阻塞读操作。所以对 COW 的优化,也应该具有极高的读效率。

两个数组当成一个数组使用的这个优化,对读操作最明显的影响是,在每次读的时候,都需要判断读操作发生在两个数组中的哪一个上面。

对于这个问题,每次通过判断语句判断,显然是很影响效率的。

对应的解决办法是,利用 Java 底层对数组访问做的安全控制,我们在代码层面不加以判断,等底层判断发现越界并抛出数组越界异常之后,再去处理。
代码示意如下,

	public E get(int index) {
		try{
			return o[index]; // o 为第一段数组
		} catch (ArrayIndexOutOfBoundsException e) {
			return c[index-o.length]; // c 为第二段数组
		}
	}

可以看到,这样做,对于发生在比较长的第一段数组中的读完全没有任何影响。但对于读第二段较短的数组,由于需要捕获数组越界异常,并做一个减法下标映射,所以会稍微影响读效率。

另外,还有一个小问题是,最终抛出的越界异常是针对第二段数组的,给出的越界信息会不正确。不过这个小问题不关乎大局,完全可以忽略。
纠正数组越界信息,代码示意如下:

	public E get(int index) {
		try{
			return o[index]; // o 为第一段数组
		} catch (ArrayIndexOutOfBoundsException e) {
			try{
				return c[index-o.length]; // c 为第二段数组
			} catch (ArrayIndexOutOfBoundsException e1) {
				// 抛出正确的越界信息
				throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

3.2 读的正确性问题

上一个问题的解决方案,又会引发出另一个问题。
即,在读第二段数组中的元素时,第一段数组的长度不能变长。

举例说明,假设第一段数组长度为 10,第二段数组长度为 5。
此时用户想要 get(10),读第一段数组时,发现越界转而去读第二段数组。
此时另一个用户在第一段数组中添加元素,并且已经执行完拷贝、增加、切换数组的操作。那么此时第一段数组的长度就变成了 11。前一个用户的 get(10) 执行到 c[index-o.length] 时,index-o.length 为 -1,此时变成想要访问第二段数组下标为 -1 的元素,显然会抛出原本不应该抛出的数组越界异常。

解决方案一:
不允许在第一段数组上增加元素。

解决方案二:
捕获发生在第二段数组上的数组越界异常。
如果发现是第一段数组变长所致,尝试重新读。有点自旋锁的意思。
代码示意如下,

	public E get(int index) {
		try{
			return o[index]; // o 为第一段数组
		} catch (ArrayIndexOutOfBoundsException e) {
			int i = 0;
			try{
				return c[i = index-o.length]; // c 为第二段数组
			} catch (ArrayIndexOutOfBoundsException e1) {
				if(i < 0) return get(index); // 重新读
				else throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

对于读多写少的场景,完全可以这样递归读。

如果写操作比较频繁,也可以对第一段数组加锁,然后再次进行读操作。
代码示意如下,

	public E get(int index) {
		try{
			return o[index]; // o 为第一段数组
		} catch (ArrayIndexOutOfBoundsException e) {
			int i = 0;
			try{
				return c[i = index-o.length]; // c 为第二段数组
			} catch (ArrayIndexOutOfBoundsException e1) {
				if(i < 0){
					synchronized (o) { // 此时,读操作有一定概率被阻塞
						return get(index);
					}
				}else throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

这样,大多数的读,都和之前一样。只有在读第二段数组时,第一段数组变长,并且 index-o.length 为负这种情况,才会阻塞读操作。

四、示意代码

时间关系,只实现了构造方法和基本的增删改查。

package main;

import java.util.Arrays;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;

public class IncrementCOW<E> {

	// 成员变量
	private volatile Object[] o;
	private CopyOnWriteArrayList<E> c = new CopyOnWriteArrayList<>();

	// 构造方法
	public IncrementCOW(){
		o = new Object[0];
	}
	
	public IncrementCOW(E[] toCopyIn){
		o = Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class);
	}

	public IncrementCOW(Collection<? extends E> c){
		Object[] elements = c.toArray();
		// c.toArray might (incorrectly) not return Object[] (see 6260652)
		if (elements.getClass() != Object[].class)
			elements = Arrays.copyOf(elements, elements.length, Object[].class);
		o = elements;
	}

	// 普通方法
	public int size(){
		return o.length + c.size();
	}

	@SuppressWarnings("unchecked")
	private E get(Object[] a, int index) {
		return (E) a[index];
	}
	
	public E get(int index) {
		try{
			return get(o, index);
		}catch (ArrayIndexOutOfBoundsException e) {
			int i = 0;
			try{
				return c.get(i = index-o.length);
			}catch (ArrayIndexOutOfBoundsException e1) {
				if(i < 0) return get(index); // 不加锁的方式
				else throw new ArrayIndexOutOfBoundsException(index);
			}
		}
	}

	/**
	 * 没有同时给 o 和 c 加锁
	 * 所以在 set c 的时候,index 可能不是逻辑数组实时的下标
	 * 只保证 index 是用户调用 set 方法后逻辑数组一个快照的下标
	 * */
	public E set(int index, E element) {
		if(index < 0) throw new IndexOutOfBoundsException("Index: "+index);
		int i;
		synchronized (o) {
			i = index-o.length;
			if(i < 0){
				E oldValue = get(o, index);
				if(oldValue != element){
					Object[] newElements = Arrays.copyOf(o, o.length);
                	newElements[index] = element;
                	o = newElements;
				}
                return oldValue;
			}
		}
		return c.set(i, element);
	}

	public void add(E e) {
		c.add(e);
	}

	// 同 set 方法,没有同时给 o 和 c 加锁
	public void add(int index, E element) {
		if(index < 0) throw new IndexOutOfBoundsException("Index: "+index);
		int i;
		synchronized (o) {
			i = index-o.length;
			if(i < 0){
				Object[] newElements = new Object[o.length + 1];
				System.arraycopy(o, 0, newElements, 0, index);
				System.arraycopy(o, index, newElements, index + 1,
						-i);
				newElements[index] = element;
				o = newElements;
			}
		}
		c.add(i, element);
	}

	// 同 set 方法,没有同时给 o 和 c 加锁
	public E remove(int index) {
		if(index < 0) throw new IndexOutOfBoundsException("Index: "+index);
		int i;
		synchronized (o) {
			i = index-o.length;
			if(i < 0){
				E oldValue = get(o, index);
				int numMoved = -i - 1;
	            if(numMoved == 0)
	                o = Arrays.copyOf(o, o.length - 1);
	            else{
	                Object[] newElements = new Object[o.length - 1];
	                System.arraycopy(o, 0, newElements, 0, index);
	                System.arraycopy(o, index + 1, newElements, index,
	                                 numMoved);
	                o = newElements;
	            }
	            return oldValue;
			}
		}
		return c.remove(i);
	}

}

你可能感兴趣的:(Java)