CopyOnWriteArrayList&CopyOnWriteArraySet_写入时复制

  在非并发中,一般常用的集合有ArrayList,HashSet,HashMap。并发中有ConcurrentHashMap,CopyOnWriteArrayList,CopyOnWriteArraySet来保证线程的安全。今天就学习下CopyOnWriteArrayList和CopyOnWriteArraySet。

  CopyOnWrite,顾名思义在Write的时候才会Copy,Read的时候是不会Copy的,这样我们就理解了什么叫写入时复制,即写的时候,我们对写操作加一个锁,用来保证数据的安全性,读操作是不会修改数据的,所以不需要加锁

CopyOnWriteArrayList:

  这是线程安全的有序集合,在CopyOnWriteArrayList中,定义了Lock用来实现锁,一个volatile Object数组用来存储数据。

final transient ReentrantLock lock = new ReentrantLock();

private transient volatile Object[] array;

  再看下常用的add,remove方法,可以看到在对添加或者删除之前都是加了Lock锁的,在操作完成后再释放锁:

  
    /**********/
    final void setArray(Object[] a) {
        array = a;
    } 
    /*****add*****/
   public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();    //上锁
        try {
            Object[] elements = getArray();    //拿到当前的数组
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);    //复制出一个新的数组
            newElements[len] = e;
            setArray(newElements);    //复制新数组
            return true;
        } finally {
            lock.unlock();    //释放锁
        }
    }   
        /*****remove*****/
    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);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

  而在get方法中,却是简单粗暴,直接通过数组下标拿到了值:

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

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

  这样我们理解CopyOnWriteArrayList还是挺简单的,在读的时候不加锁,添加和删除加锁。很适合读多写少的数据类型,并发效率更高。但也有些问题,比如我们在读数据时,其他线程在更改旧数据,这样会读取旧数据。添加和删除会频繁的复制数组,可能浪费内存引起GC。

CopyOnWriteArraySet:

  HashSet和CopyOnWriteArraySet都有同一个父亲(AbstractSet),它们又喜欢了不同的人(HashMap&CopyOnWriteArrayList)。在HashSet中,底层是定义了一个HashMap,利用HashMap的键不可重复性实现了Set数据的不可重复性。而在CopyOnWriteArraySet中,底层是基于CopyOnWriteArrayList实现的。我们了解了CopyOnWriteArrayList后,理解CopyOnWriteArraySet就简单很多了,因为它的方法基本都是调用CopyOnWriteArrayList的方法。

  先看一下它的无参构造方法,直接new了一个CopyOnWriteArrayList:

    private final CopyOnWriteArrayList<E> al;

    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

  再看看Set里常用的方法,add、remove、contains、clear。发现全是调用CopyOnWriteArrayList的方法,改都不带改的,真的是爱的深沉啊。

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }

    public boolean remove(Object o) {
        return al.remove(o);
    }

    public boolean contains(Object o) {
        return al.contains(o);
    }

    public void clear() {
        al.clear();
    }

  看到这里,我们还是没看到CopyOnWriteArraySet是如何实现不可重复性的,继续点进add里,在addIfAbsent里会根据indexOf的返回值判读e元素是已经有了,还是执行add操作。而在indexOf里,我们可以看到是for循环遍历找下标,我和我的小伙伴都惊呆了!居然是for循环!无语>"P{$%*&^*#$^^>:"%^&$!@#

这体现出了CopyOnWriteArraySet的弊端,适合数据量小的集合,大数据量下的for循环、复制数组谁遭的住啊!

    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        //这里根据indexOf的返回值判断是否存在,若大于等于0则存在
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }
    
    //indexOf:循环遍历得到e的下标,不存在则返回-1
    private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) {            
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }

    /*
    * A version of addIfAbsent using the strong hint that given
    * recent snapshot does not contain e.
    *最近的不包含e的addIfAbsent快照
    */
    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

  看到这里,基本就了解CopyOnWriteArraySet,毕竟除了不可重复性,和CopyOnWriteArrayList几乎一模一样的。虽然没有代码,我们还是可以想到CopyOnWriteArraySet的remove怎么实现,第一步,先用indexOf找要删除的元素e的下标,如果没找到就返回false,有则执行第二步。第二步,上锁,复制数组,删除元素e,再把新数组复制回来覆盖,释放锁。

posted @ 2020-06-15 14:44  real_zhui  阅读(371)  评论(0)    收藏  举报