CopyOnWriterArrayList

12 篇文章 0 订阅

CopyOnWrite

  • CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
  • 那为什么不直接修改,而是要拷贝一份修改呢?
    这是为了在“读”的时候不加锁。(以空间换时间的策略)
  • 为了提升读取的效率,修改时不在原数据上修改,而是在复制的数组上修改,改完之后再设置回来,这样做就不会阻塞读的线程

CopyOnWriteArrayList

  • CopyOnWriteArrayList介绍
  1. CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
  2. CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
  3. CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
  • CopyOnWriteArrayList 缺点:
  1. 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的FullGC,应用响应时间也随之变长。
    针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
  2. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

在这里插入图片描述

CopyOnWriteArrayList的核心数据结构是一个数组,代码如下:

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess,Cloneable, java.io.Serializable {
    final transient ReentrantLock lock = new ReentrantLock();

    /** 数组,只能通过 getArray/setArray 访问,加上transient不让其被序列化,加上volatile修饰来保证多线程下的其可见性和有序性 */
    private transient volatile Object[] array;
}

构造函数

    public CopyOnWriteArrayList() {
       //默认创建一个大小为0的数组
        setArray(new Object[0]);
    }
	//setArray只是换了一下引用地址
    final void setArray(Object[] a) {
        array = a;
    }
    final Object[] getArray() {return array;}
	
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        //如果当前集合是CopyOnWriteArrayList的类型的话,直接赋值给它
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
         	//否则调用toArra()将其转为数组   
            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);
        }
        //设置数组
        setArray(elements);
    }
	
    public CopyOnWriteArrayList(E[] toCopyIn) {
        //将传进来的数组元素拷贝给当前数组
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

下面是CopyOnArrayList的几个“读”方法:

final Object[] getArray() {
	return array;
}
//
public E get(int index) {
	return elementAt(getArray(), index);
}
public boolean isEmpty() {
	return size() == 0;
}
public boolean contains(Object o) {
	return indexOf(o) >= 0;
}
public int indexOf(Object o) {
	Object[] es = getArray();
	return indexOfRange(o, es, 0, es.length);
}
private static int indexOfRange(Object o, Object[] es, int from, int to) {
	if (o == null) {
		for (int i = from; i < to; i++)
			if (es[i] == null)
				return i;
	} else {
		for (int i = from; i < to; i++)
			if (o.equals(es[i]))
				return i;
	}
	return -1;
}

既然这些“读”方法都没有加锁,那么是如何保证“线程安全”呢?答案在“写”方法里面。
add

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	// 锁对象
	final transient Object lock = new Object();
	//数据加在数组后面
	public boolean add(E e) {
        //使用ReentrantLock上锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //调用getArray()获取原来的数组
            Object[] elements = getArray();
            int len = elements.length;
            //复制老数组,得到一个长度+1的数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //添加元素,在用setArray()函数替换原数组
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
	//在指定索引处添加数据
	public void add(int index, E element) {
		synchronized (lock) { // 同步锁对象
			Object[] es = getArray();
			int len = es.length;
			if (index > len || index < 0)
				throw new IndexOutOfBoundsException(outOfBounds(index,len));
			Object[] newElements;
			//需要向后移动的元素
			int numMoved = len - index;
			if (numMoved == 0)
				newElements = Arrays.copyOf(es, len + 1);
			else {
				newElements = new Object[len + 1];
				//CopyOnWrite,写的时候,先拷贝一份之前的数组。
				System.arraycopy(es, 0, newElements, 0, index); 
				System.arraycopy(es, index, newElements, index + 1,numMoved);
			}
			newElements[index] = element;
			// 把新数组赋值给老数组
			setArray(newElements); 
		}
	}
}

remove

    public boolean remove(Object o) {
        Object[] snapshot = getArray();
        int index = indexOf(o, snapshot, 0, snapshot.length);
        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 (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;
                        break findIndex;
                    }
                }
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                index = indexOf(o, current, index, len);
                if (index < 0)
                    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();
        }
    }


CopyOnWriteArraySet

CopyOnWriteArraySet 就是用 Array 实现的一个 Set,保证所有元素都不重复。其内部是封装的一个CopyOnWriteArrayList。

public class CopyOnWriteArraySet<E> extends AbstractSet<E> implements java.io.Serializable {
	// 新封装的CopyOnWriteArrayList
	private final CopyOnWriteArrayList<E> al;
	public CopyOnWriteArraySet() {
		al = new CopyOnWriteArrayList<E>();
	}
	public boolean add(E e) {
		return al.addIfAbsent(e); // 不重复的加进去
	}
}

总结

  • CopyOnWriterArrayList的源码还是比较简单的,重要的是以空间换时间的策略,这一点在并发编程的应用,如LongAdder,当线程不存在竞争的时候,首先将值写入到base中,当线程之间有竞争时内部维护了一个base值和一个cell数组。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值