CopyOnWriteArrayList的底层原理分析

介绍

CopyOnWriteArrayList是Java并发包中提供的一个并发容器,它是个线程安全且读操作无锁的ArrayList,写操作则通过创建底层数组的新副本来实现,是一种读写分离的并发策略,我们也可以称这种容器为"写时复制器",Java并发包中类似的容器还有CopyOnWriteSet。

源码分析:

add(E e)方法

public boolean add(E e) {
	//加锁
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		//获取数组,我们知道ArrayList底层是基于数组实现的
		Object[] elements = getArray();
		//获取数组的长度
		int len = elements.length;
		//拷贝一个新数组,在原数组的长度上加1
		Object[] newElements = Arrays.copyOf(elements, len + 1);
		//将要添加的值放到新数组的最后一位
		newElements[len] = e;
		//将属性指向新数组
		setArray(newElements);
		return true;
	} finally {
		lock.unlock();
	}
}

//getArray方法
final Object[] getArray() {
	return array;
}

//setArray方法
final void setArray(Object[] a) {
	array = a;
}

这是add的逻辑,主要就是将底层的数组拷贝一份,然后在新数组上进行添加,添加完成后将底层的数组指向新数组,返回。这个操作是要加锁的

remove(int index)

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);
		//计算index下标之后的元素的个数
		int numMoved = len - index - 1;
		//这个值为0的时候说明删除的是最后一个元素,
		//直接拷贝数组将前n-1个值复制进去返回即可
		if (numMoved == 0)
			setArray(Arrays.copyOf(elements, len - 1));
		else {
			//删除的不是最后一个元素,创建新数组
			Object[] newElements = new Object[len - 1];
			//将原数组从0下标开始,index个元素复制到新数组中  
			System.arraycopy(elements, 0, newElements, 0, index);
			//将原数组中从index+1下标开始,numMoved个元素复制到新数组中    
			System.arraycopy(elements, index + 1, newElements, index,
											 numMoved);
			//将引用指向新数组    
			setArray(newElements);
		}
		//返回被删除的值
		return oldValue;
	} finally {
		//解锁
		lock.unlock();
	}
}

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

这是根据数组下标删除元素,int numMoved = len - index - 1;这句可能有的人不懂,解释一下,
如果index = len - 1 (数组中最后一个元素),那么numMoved = 0;
如果index = 0 (数组中第一个元素),那么numMoved = len - 1;
所以显而易见,numMoved 表示的就是要删除的元素的下标index后要拷贝的元素个数。

remove(Object o)方法

public boolean remove(Object o) {
	//获取当前数组
	Object[] snapshot = getArray();
	//获取要删除的值o的下标
	int index = indexOf(o, snapshot, 0, snapshot.length);
	//如果找不到元素下标,返回false
	//如果找到元素下标,执行删除操作
	return (index < 0) ? false : remove(o, snapshot, index);
    
}

//indexOf方法
private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
	//如果o为null,获取数组中第一个为null的元素的下标
	if (o == null) {
		for (int i = index; i < fence; i++)
			if (elements[i] == null)
				return i;
	} else {
	//如果不为null,返回数组中第一个zhi为o的元素的下标
		for (int i = index; i < fence; i++)
			if (o.equals(elements[i]))
				return i;
	}
	//找不到元素,返回-1
	return -1;
}

//remove方法
private boolean remove(Object o, Object[] snapshot, int index) {
	//加锁
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		//获取当前的数组
		Object[] current = getArray();
		//获取当前数组的长度
		int len = current.length;
		//如果取出的快照数组和当前数组不是同一个数组,
		//说明,在删除期间,有别的线程动过数组
		//findIndex -> 给代码块做个标记,可以退出代码块
		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])) {//(1)
					index = i;
					break findIndex;
				}
			}
			if (index >= len)//(2)
				return false;
			if (current[index] == o)//(3)
				break findIndex;
			index = indexOf(o, current, index, len);//(4)
			if (index < 0)//(5)
				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();
	}
}

findIndex: {}这个用法用的比较少,简单理解就是findIndex给"{}"里面的代码块做个标记,如果break findIndex;表示直接退出这个代码块,代码块里的代码不执行了,直接执行下面的逻辑。
不管是根据索引来删除元素还是直接删除元素,源码里面都是加了锁,创建了新数组来操作的。
这个代码块里,其实分析了被动过的数组的几种情况:

  1. 数组有删除,并且index位置的元素没有修改和删除,有删除的情况下,len和index有两种大小关系:
    a. index>len,说明原先的数组[0,index]区间内肯定有删除(如果只有[index,len]区间内有删除,len不可能小于index)。那么代码走到1为ture,更新index,并将除了index位置的元素复制到新数组中返回
    b. index<len,不用管[index,len]区间内有没有删除元素
    ⅰ. 如果[0,index]区间内有删除元素,逻辑同a
    ⅱ. 如果[0,index]区间内没有删除元素,说明只有[index,len]区间内有删除元素,则只有将除index位置的元素之外的其他元素复制到新数组返回
  2. 数组有删除,并且index位置的元素被修改或删除。那么代码(1)为false,如果index>=len(代码2),直接返回false,如果index<len,重新定位元素o的位置(代码4),找不到,直接返回false(代码5)
  3. 数组没有删除,并且index位置的元素没有修改和删除。则只要将除index位置的元素之外的其他元素复制到新数组返回
  4. 数组没有删除,并且index位置的元素被修改,重新定位o的位置(代码4),找不到直接返回false(代码5)

get(int index)方法

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

get方法,没有加锁。

总结:

CopyOnWrite添加和删除的时候都是复制了一个新数组,在新数组上添加删除,而读操作则没有限制。
所以优点显而易见,高并发下读写不会抛出并发修改异常,读写分离。
缺点就是高并发下,刚添加的元素可能不会马上读到,刚删除的元素可能还能读到

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值