2019独角兽企业重金招聘Python工程师标准>>>

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList相当于线程安全的ArrayList,通过增加写时复制语义来实现线程安全性。底层也是通过一个可变数组来实现的。但是和ArrayList不同的时,它具有以下特性:

  1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突
  2. 支持高效率并发且是线程安全的
  3. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大
  4. 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作
  5. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

array: 保存了列表中的数据

lock: 修改时加锁,用于保证线程安全

底层数据结构依然是数组,相比较于ArrayList而言,少了一个表示数组长度的size变量,获取列表长度是通过下面的方法

首先来看一下构造函数,如下所示:

private volatile transient Object[] array;

final Object[] getArray() {
        return array;
    }
    final void setArray(Object[] a) {
        array = a;
    }
    // -----开始构造函数----------------------------
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] 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);
    }
    // Creates a list holding a copy of the given array.
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

使用一个指向volatile类型的Object数组来保存容器元素。构造函数中都会根据参数值重新生成一个新的数组。

1.添加元素

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();
        }
    }

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

ArrayList新增元素时,可能导致数组扩容;CopyOnWriteArrayList在列表的修改时,采用数组拷贝,在新的数组上进行操作,从这点出发,应该不存在扩容的问题,因为每次修改都会导致数组的重新拷贝

有两点必须清楚:

第一,在”添加操作“开始前,获取独占锁(lock),若此时有需要线程要获取锁,则必须等待;在操作完毕后,释放独占锁(lock),此时其它线程才能获取锁。通过独占锁,来防止多线程同时修改数据!

第二,操作完毕时,会通过setArray()来更新volatile数组。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入;这样,每次添加元素之后,其它线程都能看到新添加的元素。

2.获取元素:

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

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

将底层volatile数组指定索引处的元素返回即可。

3.删除元素:

以remove(int index)为例,来对“CopyOnWriteArrayList的删除操作”进行说明。下面是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); // 获取volatile数组中指定索引处的元素值
        int numMoved = len - index - 1;
        if (numMoved == 0) // 如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组
            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); // 更新volatile数组
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

从删除的实现,可确定以下几点:

  • 修改加锁,确保同一时刻只有一个线程对数组进行修改

  • 修改并不是在原数组上进行的,而是创建一个新的数组,在新的数组上进行操作操作,然后将tables引用指向新的数组

  • 修改必然会涉及到数组内容的拷贝

4.遍历元素:

public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    public ListIterator<E> listIterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    public ListIterator<E> listIterator(final int index) {
        Object[] elements = getArray();
        int len = elements.length;
        if (index<0 || index>len)
            throw new IndexOutOfBoundsException("Index: "+index);

return new COWIterator<E>(elements, index);
    }

private static class COWIterator<E> implements ListIterator<E> {
        private final Object[] snapshot; // 保存数组的快照,是一个不可变的对象
        private int cursor;

private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

public boolean hasNext() {
            return cursor < snapshot.length;
        }

public boolean hasPrevious() {
            return cursor > 0;
        }

@SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

@SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

public int nextIndex() {
            return cursor;
        }

public int previousIndex() {
            return cursor-1;
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
        public void set(E e) {
            throw new UnsupportedOperationException();
        }
        public void add(E e) {
            throw new UnsupportedOperationException();
        }
    }

如上容器的迭代器中会保存一个不可变的Object数组对象,那么在进行遍历这个对象时就不需要再进一步的同步。在每次修改时,都会创建并重新发布一个新的窗口副本,从而实现了可变性。如上迭代器代码中保留了一个指向volatile数组的引用,由于不会被修改,因此多个线程可以同时对它进行迭代,而不会彼此干扰或与修改容器的线程相互干扰。

与之前的ArrayList实现相比,CopyOnWriteArrayList返回迭代器不会抛出ConcurrentModificationException异常,即它不是fail-fast机制的!

JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:

import java.util.Collection;

import java.util.Map;

import java.util.Set;

public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {

    private volatile Map<K, V> internalMap;

    public CopyOnWriteMap() {

        internalMap = new HashMap<K, V>();

    }

    public V put(K key, V value) {

        synchronized (this) {

            Map<K, V> newMap = new HashMap<K, V>(internalMap);

            V val = newMap.put(key, value);

            internalMap = newMap;

            return val;

        }

    }

    public V get(Object key) {

        return internalMap.get(key);

    }

    public void putAll(Map<? extends K, ? extends V> newData) {

        synchronized (this) {

            Map<K, V> newMap = new HashMap<K, V>(internalMap);

            newMap.putAll(newData);

            internalMap = newMap;

        }

    }

}

CopyOnWrite的应用场景

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

package com.ifeve.book;

import java.util.Map;

import com.ifeve.book.forkjoin.CopyOnWriteMap;

/**

 * 黑名单服务

 *

 * @author fangtengfei

 *

 */

public class BlackListServiceImpl {

    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(

            1000);

    public static boolean isBlackList(String id) {

        return blackListMap.get(id) == null false true;

    }

    public static void addBlackList(String id) {

        blackListMap.put(id, Boolean.TRUE);

    }

    /**

     * 批量添加黑名单

     *

     * @param ids

     */

    public static void addBlackList(Map<String,Boolean> ids) {

        blackListMap.putAll(ids);

    }

}

实现很简单,只要了解了CopyOnWrite机制,我们可以实现各种CopyOnWrite容器,并且在不同的应用场景中使用。

 代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

CopyOnWrite的缺点

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

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

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

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

多记录一句:

Collections.synchronizedList & CopyOnWriteArrayList

CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。

转载于:https://my.oschina.net/u/1054538/blog/1594618

并发容器CopyOnWriteArrayList相关推荐

  1. 【Java多线程】并发容器CopyOnWriteArrayList

    一个不安全的List 预期输出结果为10000,但是实际运行结果为9992,是因为线程不安全,应该使用synchronized同步块 不安全代码 package cn.hanquan.test;imp ...

  2. Java并发指南14:Java并发容器ConcurrentSkipListMap与CopyOnWriteArrayList

    原文出处http://cmsblogs.com/ 『chenssy』 到目前为止,我们在Java世界里看到了两种实现key-value的数据结构:Hash.TreeMap,这两种数据结构各自都有着优缺 ...

  3. Java集合之并发容器

    一:java中的并发容器总结 JDK提供的这些容器大部分在 java.util.concurrent 包中. ConcurrentHashMap: 线程安全的HashMap CopyOnWriteAr ...

  4. 探索JAVA并发 - 并发容器全家福!

    作者:acupt,专注Java,架构师社区合伙人! 14个并发容器,你用过几个? 不考虑多线程并发的情况下,容器类一般使用ArrayList.HashMap等线程不安全的类,效率更高.在并发场景下,常 ...

  5. 高并发编程_高并发编程系列:7大并发容器详解(附面试题和企业编程指南)...

    不知道从什么时候起,在Java编程中,经常听到Java集合类,同步容器.并发容器,高并发编程成为当下程序员需要去了解掌握的技术之一,那么他们有哪些具体分类,以及各自之间的区别和优劣呢? 只有把这些梳理 ...

  6. 并发编程-14线程安全策略之并发容器(J.U.C)中的集合类

    文章目录 J.U.C总览 脑图 概述 并发容器特性 示例 ArrayList对应的线程安全的并发容器类CopyOnWriteArrayList (线程安全) HashSet对应的线程安全的并发容器类C ...

  7. 高并发之并发容器详解(从入门到超神)

    一.ConcurrentHashMap 在上面已经提到过ConcurrentHashMap,ConcurrentHashMap相比Hashtable能够进一步提高并发性,其原理图如下: HashMap ...

  8. 高并发之并发容器,了解多少(从入门到超神)

    点击上方"好好学java",选择"置顶公众号" 优秀学习资源.干货第一时间送达! 精彩内容 java实战练习项目教程 2018微服务资源springboot.s ...

  9. java-并发-并发容器(3)

    同样注意内层的第一个for循环,里面有语句int c = segments[i].count; 但是c却从来没有被使用过,即使如此,编译器也不能做优化将这条语句去掉,因为存在对volatile变量co ...

最新文章

  1. Grunt安装中遇到的问题汇总
  2. 指令—— 数据绑定指令||数据响应式||双向数据绑定指令
  3. 初步了解win32界面库DuiLib
  4. Matlab中gradient函数的使用方法
  5. 前端基础3-1——JavaScript
  6. 4月14日,微软发布例行月度安全更新
  7. 原生JS实现粘贴到剪贴板
  8. iOS音频播放 (一):概述 转
  9. 直线插补计算过程_【计鹏视角】风速数据插补对发电量的影响
  10. 获取Android 光感Sensor的值
  11. 以太网交换机工作原理
  12. NetscalerSSL证书的导入和link
  13. FIND_IN_SET使用场景
  14. Arduino操作记录---雨滴传感器的使用
  15. BTA前瞻 | 这家区块链公司币圈链圈通吃!专访井通科技CTO杨建新
  16. 10计算机管理员权限获得,Win10永久获取管理员权限的方法
  17. 【企业数字化转型】数字化转型的本质(学习笔记)
  18. 项目启动报错,找不到文件
  19. 预算受限拍卖论文第二章整理
  20. 0xc0150002错误

热门文章

  1. 野指针和悬空指针的形成原因和如何避免!
  2. 下述程序有什么问题?
  3. 办公文档加密,企业文档加密,强制性透明加密技术,fasoft
  4. hdu 1753大小数相加
  5. flask+sqlite3+echarts3+ajax 异步数据加载
  6. html5 如何实现客户端验证上传文件的大小
  7. DirectShow基础编程 最简单的源Filter的编写步骤 (转)
  8. 动态子类化CComboBox以得到子控件EDIT及LISTBOX
  9. 问题小结(2)-dialog内容动态变化(调用系统方法时)
  10. 艾伟:【翻译】使用ASP.NET 2.0记录错误