CopyOnWrite分析

CopyOnWrite是一种写时复制的并发机制,用于实现读多写少的场景。它通过在修改时复制底层数组来确保并发读操作的不间断。在Java的`concurrent`包中有`CopyOnWriteArrayList`和`CopyOnWriteArraySet`两种实现。读操作如size()、indexOf()等无须加锁,而写操作如set()、add()会加锁并复制数组。由于可能存在读旧数据的情况,因此CopyOnWrite不保证实时性,但保证最终一致性。适用场景需权衡读取延迟与数据一致性需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

定义:

CopyOnWrite机制称为写时复制,理解起来很简单,就是执行修改操作时进行底层数组复制,使得修改操作在新的数组上进行,不妨碍原数组的并发读操作,复制修改完成后更新原数组引用变量。

原理:

每个修改之前都加上ReentrantLock使并发写操作互斥执行,避免多次数据复制修改。所以能够实现读写分离,但是写写同步执行。读写并发环境中,在将原数组对象引用变量更新为复制修改完成后的数组对象之前,读的数组对象和写的数组对象可能不一致,所以不能保证实时数据一致性,只能保证最终一致性。

实现:

在concurrent包下CopyOnWrite机制的实现有两种,CopyOnWriteArrayList和CopyOnWriteArraySet。其实CopyOnWriteArraySet就是一个CopyOnWriteArrayList,不过就是在方法中避免重复数据而已,甚至这些避免重复数据的函数也是在CopyOnWriteArrayList中定义的,CopyOnWriteArraySet中只是包含一个CopyOnWriteArrayList的属性,然后在方法上做个包装,除了equals方法外,其他当前类中的所有函数都是调用的CopyOnWriteArrayList的方法,所以严格来讲可以使用一个CopyOnWriteArrayList作为具有Set特性的写时复制数组(不过就是没有继承AbstractSet)。

CopyOnWriteArrayList中的属性:

//序列化序列号,不定义的话,会自动生成
private static final long serialVersionUID = 8673264195747942595L;
transient final ReentrantLock lock = new ReentrantLock();
private volatile transient Object[] array;
//看到这里的数组以transient修饰,就应该知道后面一定有自定义的writeObject和readObject方法
因为主要操作就是lock加锁,读写、复制数组,所以重点照看就是lock、array这两个对象。

后面常见的array操作:

final Object[] getArray() {
        return array;
    }
final void setArray(Object[] a) {
        array = a;
    }
在array数组上的操作就这样两种类型:

1.不加锁的size(),isEmpty(),indexOf(),contains(),get()。称为读操作,非修改操作

2.加锁的set(),add(),remove()。称为修改操作

最简单的无检查读操作(当然这个无检查是程序无检查,JVM自然是需要检查的,毕竟java是安全语言):

public E get(int index) {
        return get(getArray(), index);
    }
private E get(Object[] a, int index) {
        return (E) a[index];
    }
以set操作为例:

public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁执行
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);//array中即当前数组的对应位置元素oldValue

            if (oldValue != element) {//元素需要替换时
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);//array的分身,新数组对象
                newElements[index] = element;
                setArray(newElements);//更新操作  array=newElements
            } else {//不需要替换元素,其实不太明白下面那句话的意思
				//并不是完全的空操作;保证volatile写操作的语义
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
			/*关于array为什么使用volatile修饰的个人理解:
			我们知道在jdk5后,将volatile修饰单例对象作为实现DCL的一种方式,使用的就是volatile的
			编译后指令形成内存屏障,禁止指令重排序的特点,如下

			private static volatile Instance instance=null;//单例对象
			public static Instance getInstance(){
				if(instance==null){
					synchronized(DoubleCheckedLocking.class){
						if(instance==null){
							instance=new Instance();//在当前步骤,使得Instance实例构造在instance变量赋值
							//之前完成,禁止虚拟机或处理器的重排序,从而保证instance!=null时,对象已经构造完成
						}
					}
				}
			}
			因为读操作是不需要加锁执行的,可以实时访问array对象,所以给array加volatile修饰是基于上面相同的思考
			保证array的修改完成在对array变量的更新之前
			*/
            return oldValue;
        } finally {//为了忘记释放锁或者中间抛出什么异常,尽量把释放锁操作放在finally中
            lock.unlock();
        }
    }

总结:

CopyOnWrite机制保证读写分离,在读多写少的并发环境下可以起到很好的作用,但是因为是写时复制,所以不能保证数据的实时性,而只能保证最终一致性。

个人想法:

1. CopyOnWrite可以保证读操作时,面对的可能是旧数组数据,虽然保证了写的同时进行读操作,但是可能会察觉不到最新的值。此处可以加上CAS的原理,如果读的不是最新的值,一般情况下无所谓的话,那就读旧数据好了,没什么大不了的;如果要求比较高,则可以sleep一会再进行读操作。

2. 如果不能察觉出什么情况下,读旧数据也无所谓,什么情况下哪怕牺牲一点时间(或者自旋或者休眠)也要等待新数据,可以设置一个修改量,量化对array数组的修改程度。以此判断是否等待或者直接读了走人。

示例:

class CopyOnWriteArrayList_t<E>{
	private final ReentrantLock lock = new ReentrantLock();	
	private volatile Object[] array;
	private volatile int modCount=0;//修改次数

	public CopyOnWriteArrayList_t(){
		array=new Object[0];
	}
	public E get(int index){//不作范围检查
		if(modCount<=(array.length/10)){//如果修改的程度小于原array数组的十分之一,则直接读取
			return (E)array[index];
		}
		while(modCount!=0){
			//等待
			/*或者执行
			lock.lock();
			lock.unlock();
			*/
		}
		return (E)array[index];
	}
	private Object[] getArray(){
		return array;
	}
	private void setArray(Object[] a){
		array=a;
	}
	public boolean add(E e) {//单个修改,虽然add只是单个添加
        final ReentrantLock lock = this.lock; //但是如果array数组很少或者为空,则添加一个元素影响不可消除
        lock.lock();
        try {
			modCount++;//待修改次数增加,表示即将进行的修改操作
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
			newElements[len] = e;
			setArray(newElements);
			modCount=0;//修改次数清零
            return true;
        } finally {
            lock.unlock();
        }
    }
	public void removeRange(int fromIndex, int toIndex) {//多个修改,容易造成修改前后array差别较大
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
			modCount=toIndex - fromIndex;//待修改次数,表示即将进行的修改操作
            Object[] elements = getArray();
            int len = elements.length;

            if (fromIndex < 0 || toIndex > len || toIndex < fromIndex)
                throw new IndexOutOfBoundsException();
            int newlen = len - (toIndex - fromIndex);
            int numMoved = len - toIndex;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, newlen));
            else {
                Object[] newElements = new Object[newlen];
                System.arraycopy(elements, 0, newElements, 0, fromIndex);
                System.arraycopy(elements, toIndex, newElements,
                                 fromIndex, numMoved);
                setArray(newElements);
				modCount=0;//修改次数清零
            }
        } finally {
            lock.unlock();
        }
    }
	public String toString() {
        return Arrays.toString(array);
    }
}

这里的get方法判断:等待即将发生的修改完成,还是直接在原array数组上执行读操作,只是根据修改程度进行选择,很粗糙,没有考虑到执行任务的类型,也没有选择等待的方式,sleep休眠或者yield让步,又或者满足时间及时性的notify唤醒操作。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值