zoukankan      html  css  js  c++  java
  • JUC之CopyOnWriteArrayList和CopyOnWriteArraySet

    一、简介

    CopyOnWriteArrayList简介

      ArrayList是一种 “列表” 数据结构,其底层是通过数组来实现元素的随机访问。JDK1.5之前,如果想要在并发环境下使用 “列表”,一般有以下3种方式:

    1. 使用Vector

    2. 使用Collections.synchronizedList返回一个同步代理类;

    3. 自己实现ArrayList的子类,并进行同步/加锁

      前两种方式都相当于加了一把“全局锁”,访问任何方法都需要首先获取锁。第3种方式,需要自己实现,复杂度较高。

      JDK1.5时,随着JUC引入了一个新的集合工具类——CopyOnWriteArrayList:

      

      大多数业务场景都是一种“读多写少”的情形,CopyOnWriteArrayList就是为适应这种场景而诞生的。

      CopyOnWriteArrayList,运用了一种“写时复制”的思想。

      通俗的理解就是当我们需要修改增/删/改)列表中的元素时,不直接进行修改,而是列表Copy,然后在新的副本上进行修改修改完成之后,在将引用原列表指向新列表。

      这样做的好处是读/写是不会冲突的,可以并发进行,读操作还是在原列表,写操作在新列表。仅仅当有多个线程同时进行写操作时,才会进行同步。

    CopyOnWriteArraySet简介

      CopyOnWriteArraySet,是另一类适合并发环境的SET工具类。从名字上可以看出,也就是基于“写时复制” 的思想。

      事实上,CopyOnWriteArraySet内部引用了一个CopyOnWriteArrayList对象,以“组合”方式,委托CopyOnWriteArrayList对象实现了所有API功能。

    二、源码分析

    CopyOnWriteArraySet基本是依靠CopyOnWriteArrayList的,所以我们只分析CopyOnWriteArrayList即可

    CopyOnWriteArrayList

    构造器

    public CopyOnWriteArrayList() { setArray(new Object[0]);}
    public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); //setArray方法进行初始化
    }
    public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
    elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
    elements = c.toArray();
    if (elements.getClass() != Object[].class)
    elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
    }

    属性

    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;
    private static final sun.misc.Unsafe UNSAFE;
    private static final long lockOffset;

    核心方法

    与构造器初始化相关的setArray方法

    final void setArray(Object[] a) { array = a; } // 设置底层数据结构数组值
    final Object[] getArray(){ return array; } //返回数据

    查询——get方法

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

    可以看到,get方法并没有加锁,直接返回了内部数组对应索引位置的值:array[index]


    添加——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);   // 内部array引用指向新数组
               return true;
           } finally {
               lock.unlock();   // 最后释放锁
           }
    }
    // 指定位置添加
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;  // 获取锁对象
        lock.lock();   //加锁
        try {
            Object[] elements = getArray();  //返回旧数组
            int len = elements.length;  //旧数组长度
            if (index > len || index < 0)  // 长度合法验证
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements; 
            int numMoved = len - index;  // 
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];  // 指定长度新的空数组
                System.arraycopy(elements, 0, newElements, 0, index);  //将旧数组范围0到index-1位置上数据,赋值给新数组
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);    // 将旧数组从位置index到最后,赋值给新数组从index+1的位置开始到最后
            }
            newElements[index] = element;  //将新数组index空位置上赋值添加的元素
            setArray(newElements);   // 内部array引用指向新数组
        } finally {
            lock.unlock();  //释放锁
        }
    }

      add方法首先会进行加锁,保证只有一个线程能进行修改;然后会创建一个新数组(大小为 n+1),并将原数组的值复制到新数组,新元素插入到新数组的最后;最后,将字段array指向新数组。

          

      上图中,ThreadB对Array的修改由于是在新数组上进行的,所以并不会对ThreadA的读操作产生影响。


    删除——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(); } }
    // 指定对象删除
    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(); } }

    CopyOnWrite的应用场景

      CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

    CopyOnWrite的缺点

    CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

      内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

      针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

      数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

     CopyOnWriteArrayList为什么并发安全且性能比Vector好

     我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

    参考:https://www.cnblogs.com/myseries/p/10877420.html

        https://segmentfault.com/a/1190000016214572

  • 相关阅读:
    p1822
    Spring框架——文件上传(SpringMVC)
    Spring框架——拦截器(SpringMVC)
    Spring框架——SpringMVC
    Spring框架——AOP
    Spring框架——SpringEL
    Spring框架——Bean与DI
    毕业实习与毕设
    Spring框架——Ioc、DI与Spring框架
    Java基础——异常和断言
  • 原文地址:https://www.cnblogs.com/FondWang/p/12146681.html
Copyright © 2011-2022 走看看