CopyOnWrite类解析-程序员宅基地

技术标签: Java  java  CopyOnWrite  CopyOnWriteArraySet  CopyOnWriteArrayList  

CopyOnWrite
  • 顾名思义,写时复制, 即做Write更新的操作时,进行复制。
  • 那么为什么要这样子做呢? 接下来我们以CopyOnWriteArrayList的源码来做分析。
CopyOnWriteArrayList
  • CopyOnWriteArrayList可以看成是ArrayList的线程安全版本,所以很多方面与ArrayList相同,我们略过一些相同的方面,先来看其主要属性
    /** 可重入锁,用来保证写安全 */
    transient final ReentrantLock lock = new ReentrantLock();

    /**  volatile数组.确保 set的时候修改引用地址的时候是原子操作 */
    private volatile transient Object[] array;
  • 两个属性,一个是数据, 一个是可重入锁来控制写。看一下构造函数。
    /**
     * 通过 set一个空Object数组进行初始化
     */
    public CopyOnWriteArrayList() {
    
        setArray(new Object[0]);
    }

    /**
     * 对于非Object类的, 转换为Object数组.再set
     */
    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);
    }

    /**
     * 将传入的array复制为 Object数组
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
    
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
  • 构造函数都比较简单, 就是set一个 Object数组,再看一下几个读操作
    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
    
        return (E) a[index];
    }
    public E get(int index) {
    
        return get(getArray(), index);
    }
    /**
     */
    public int indexOf(Object o) {
    
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length);
    }
    /**
     * 通过遍历数组来确定位置. 
     */
    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;
    }
     /**通过indexOf来确定是否包含.
     */
    public boolean contains(Object o) {
    
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length) >= 0;
    }    
  • 读方法都很简单, 获取数组之后进行相关数组读。
  • 再来看看几个比较典型的写方法, add、set、remove
    /**
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
    
        final ReentrantLock lock = this.lock;
        lock.lock();    //加锁. 防止同时有两个写线程进入.
        try {
    
            Object[] elements = getArray(); // 得到array的引用. 
            int len = elements.length;     //判断 index是否合理.
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;       //定义新数组.
            int numMoved = len - index;   //需要移动的元素数量. 即.index之后的元素都需要移动一个位置.
            if (numMoved == 0)    //插到最后.
                newElements = Arrays.copyOf(elements, len + 1);  //copy原数组,并设置copy后的新数组长度为 原数组长度+1.
            else {
    
                newElements = new Object[len + 1];  //新数组比原数组多一个元素
                System.arraycopy(elements, 0, newElements, 0, index);   //copy前段.
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);  //copy index之后的后段.
            }
            newElements[index] = element;    //在目标位置设值.
            setArray(newElements);      //将 完成写操作之后的数组刷新到引用中.
        } finally {
    
            lock.unlock();   //写操作完毕解锁.
        }
    }
    public E set(int index, E element) {
    
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
    
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
    
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
    
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
    
            lock.unlock();
        }
    }
    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();
        }
    }
  • 代码加上注释很清晰了, 需要注意的是:
    • 这里用到了 lock,目的是阻止多线程写。同一时间,写操作只能有一个,这个是毋庸置疑的。
    • 这里lock可以简单理解为, try到finally这段代码加了 synchronized , 这样子就保证了多个线程调用这些写操作时安全。
    • 另外一点, 则是这里用到了 Arrays.copyOfSystem.arraycopy 。 首先,Arrays.copyOf本质上, 调用的还是System.arrayCopy的方法, 而System.arrayCopy则是使用native代码(基于内存块的拷贝),从而使数组拷贝尽量快。
    • 至于add方法, if 里使用Arrays.copy而else使用System.arraycopy而不使用 Arrays.copyRange。 我认为是因为在Arrays.copyRange里加了一些范围之类的检查,而在这里是没有必要的,因此这里使用System.arraycopy可以减少检查,效率更高.
  • 另外这里还有一个我之前想不明白, 既然已经保证了写的安全为什么要用copy, 而不在 getArray得到的数组里直接修改就好了。已经"安全"了,还复制有必要吗?
    • 其实,这里如果不用copy只用了写lock, 只能保证写的安全,如果不用copy读是不安全的
    • 例如,迭代器正在迭代,然后就被删除了一个,那么会有问题。
    • 另外,写元素的时候是非原子操作的,而读是没有lock的(但是lock了话就和VectorCollections.synchronizedList没有太大区别了),如果不copy, 会造成同时读写有问题。
    • 这就是为什么写已经安全了还要用copy的原因,是为了保证读也是安全的。
简单整理一下

在上面,我们发现, 写的时候不单有锁,并且还会进行copy, 可以发现:

1,写操作效率其实很低, 对于数据量比较大, 并且写操作比较频繁的场景是很不合适的。
2,读和写其实是分隔开了的, 除了写在同步时(setArray), 这两种操作不会互相影响。因此,写时复制在多线程频繁读的场景是比较合适的。在这种情景,该类的效率是大于Vector的, 读线程越多,表现越明显

另外,CopyOnWrite这里还有一个类, CopyOnWriteArraySet不重复元素的集合, 其底层其实使用 CopyOnWriteArrayList, 所以基本和CopyOnWriteArrayList一样的, 可以自行浏览一下源码。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/dansam/article/details/89040766

智能推荐

HCP Lab 12篇论文入选世界顶级计算机视觉会议 CVPR 2019-程序员宅基地

文章浏览阅读1k次。本文由中山大学人机物智能融合实验室(HCP Lab)特约供稿。全球计算机视觉三大顶会之一CVPR 2019(IEEE Conference on Computer Visionand Pattern Recognition) 于 6月 16~20日 在美国洛杉矶如期举办。CVPR 作为计算机视觉三大顶级会议之一,一直以来都备受关注。被 CVPR 收录的论文更是代表了计算机视觉领域的最新发展...

ubuntu14.04安装gcc失败_没有可用的软件包 gcc-multilib,但是它被其它的软件包引用了。 这可能意味着这个-程序员宅基地

文章浏览阅读4.7k次,点赞8次,收藏9次。安装又按不了->更新又不行->怎么办gcc程序“gcc”尚未安装。 您可以使用以下命令安装:sudo apt-get install gccweifc@ubuntu:~/demo$ sudo apt-get install gcc正在读取软件包列表… 完成正在分析软件包的依赖关系树正在读取状态信息… 完成现在没有可用的软件包 gcc,但是它被其它的软件包引用了。这可能..._没有可用的软件包 gcc-multilib,但是它被其它的软件包引用了。 这可能意味着这个

引入rxtx串口通信jar及native path的坑_rxtx version mismatch-程序员宅基地

文章浏览阅读2.7k次。首先,到http://rxtx.qbang.org/wiki/index.php/Download下载相应zip包,由于发帖时2.2版本不稳定(里面是jar是2.1版本,但dll是2.2版本,造成版本冲突,报错:RXTX Version mismatch),建议使用2.1-7r2版本。请参见本博客另一篇博文点击打开链接,将zip中的jar安装到maven仓库里。在pom文件中加入引用。_rxtx version mismatch

蓝绿色部署-程序员宅基地

文章浏览阅读299次。传统上,我们通过替换当前版本来部署新版本。 旧版本停止了,新版本被替换了。 这种方法的问题是从旧版本停止运行到新版本完全运行之间的停机时间。 无论您尝试执行此过程的速度如何,都会有一些停机时间。 那可能只有一毫秒,也可能持续数分钟,在极端情况下甚至可能长达数小时。 具有整体式应用程序会带来其他问题,例如,需要等待相当长的时间才能初始化应用程序。 人们试图以各种方式解决此问题,并且大多数人使用..._不可变部署 蓝绿部署

android反射获取系统属性值_android userhandle.system需反射获取-程序员宅基地

文章浏览阅读751次。/** * 获取当前系统的版本名称 * @return */ public String getSystemVersionName(){ String ver = null; try { systemProperties_get = Class.forName(&amp;quot;android.os.System..._android userhandle.system需反射获取

java中sleep()和wait()区别_sleep和wait-程序员宅基地

文章浏览阅读2w次,点赞14次,收藏56次。1,sleep方法是Thread类的静态方法,wait()是Object超类的成员方法 2,sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notif..._sleep和wait

随便推点

【Docker】:使用docker安装redis,挂载外部配置和数据_docker run -p 6379:6379 \ -v /root/docker/redis/re-程序员宅基地

文章浏览阅读2.2k次,点赞2次,收藏8次。普通安装1.拉取镜像,redis:4.0docker pull redis:4.02.创建redis容器名"redistest1",并开启持久化docker run -d -p 6379:6379 --name redistest1 redis:4.0 redis-server --appendonly yes参数说明:--appendonly yes:开启持久化挂载外部配置和数据安装1.创建目录和配置文件redis.confmkdir /dockermk_docker run -p 6379:6379 \ -v /root/docker/redis/redis.conf:/etc/redis/redis.

oracle函数_193500大写-程序员宅基地

文章浏览阅读655次。单行函数 Function name(column | expression,[arg0,arg1,arg2….]) 单行函数的分类: 字符函数:接受字符串的输入并返回字符串的值 数值函数:接受数值的输入并返回数值 日期函数:对日期数据进行操作 转换函数:从一种数据类型转换成另一种类_193500大写

Android Studio的安装和gradle的下载_andriod端下载-程序员宅基地

文章浏览阅读5.4k次,点赞4次,收藏17次。1.Android studio的安装百度搜索Android studio官网下载速度会比较慢,所以我们选择中文社区在这里选择要下载的版本,我选择的是Windows3.5.2版本下载完成之后双击安装,这里要注意,因为一般情况下我们都是用Java编程,所以安装的时候尽量选择和jdk同一个盘下。而且安装路径尽量不要出现中文,容易报错。然后一路next+finish。..._andriod端下载

Duilib vs调试程序加载资源失败,但是单独执行exe可以成功_vs 本地资源运行不起来,exe可以运行-程序员宅基地

文章浏览阅读5.3k次。在Duilib程序中,在xml中如果有加载资源文件(比如png背景图片),如果调试程序是出现黑屏,加载资源失败,但是单独执行exe文件是可以的,这是很可能是因为资源文件的位置放的不对,你可能释放到xml相同的目录,但是程序实际上加载资源文件实在项目的当前目录,把资源文件放到项目的当前目录就是可以的,这样虽然是可以的,但是感觉不太符合要求,如果需要在指定的位置加载资源文件,可以使用以下代码来设置:_vs 本地资源运行不起来,exe可以运行

Cocos Creator 如何进行断点调试?_cocos打断点-程序员宅基地

文章浏览阅读3.8k次。最近在写游戏的时候,遇到了一些奇怪的问题,由于之前写的都比较顺利,从来没有调试过代码,直到现在才发现学习 Cocos Creator 这几个月以来竟然从来没有调试过代码,于是赶紧研究了一下,发现经常用到的大概就两种:VS Code + Chrome 和 Chrome,今天就来简单的记录一下,方便和我一样不会调试的小伙伴了解一下。第一种:VS Code + Chrome1.首先创建一个 Hell..._cocos打断点

opencv基础知识连接_openmv供电怎么接线-程序员宅基地

文章浏览阅读224次。https://www.cnblogs.com/silence-cho/p/10926248.html(一)OpenCV-Python学习—基础知识_openmv供电怎么接线

推荐文章

热门文章

相关标签