HashMap都有线程安全容器我ArrayList就不配拥有?

CopyOnWriteArrayList

聪明的小伙伴从文章的标题就知道今天我们要介绍的集合时CopyOnWriteArrayList,看名字就知道是ArrayList的衍生品,为了解决ArrayList线程不安全问题而创造出来的。

在正式进入CopyOnWriteArrayList之前让我们先看一下COW(Copy On Write)

Copy On Write

fork()和exec()
fork()

fork主要用于创建一个新进程,即子进程,创建出来的新进程是通过老进程复制而来。

linux系统中的init是所有进程的父亲,即Linux上的所有进程都是通过init进程或者其子进程fork出来的

exec()

需要注意的是exec()不是一个特定的函数,而是一组函数的统称,包括了execl()、execlp()、execv()、execle()、execve()、execvp()。

其主要的作用就是用于装载一个新的程序(可执行映射)覆盖当前进程内存空间中的映像,从而执行不同的任务。

其在执行的时候会直接替换掉当前进程的地址空间。

Linux下的COW

在传统做法下,在创建了子进程后,会将父进程的数据完全拷贝进子进程,拷贝完成后,父进程和子进程之间的数据段和堆栈是相互独立的。

但是由于往往子进程会执行exec()来实现属于自己的功能,这是复制过去的数据大多数情况是没用的。为了避免无效的复制,COW技术就出现了。

COW:

  • fork创建出的子进程,与父进程共享内存空间,如果父子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程。使得创建速度加快
  • 并且如果在fork函数返回以后,子进程第一时间exec一个新的可执行映射,那么也就不会浪费时间和内存空间。

COW技术的好处:

  • 减少分配和复制资源带来的时间消耗和资源分配

COW技术的缺点:

  • 如果在fork()以后,父子进程继续进程写操作,那么会产生大量的分页错误,这样得不偿失

CopyOnWriteArrayList

通过上面的介绍我们知道了cow技术的优点以及缺点,但是要想保证ArrayList的安全性,不止有CopyOnWriteArrayList容器还有另外的两种方法,分别是:Vector和SynchronizedList。但是其都有各自的问题。

Vector和SynchronizedList问题

其中第一个问题在于,其中的方法并不是原子性的,即使他们内部的每一步的操作都是原子性的(被Synchronized修饰就可以实现原子性),但是其内部之间还可以交替进行。

出现这种问题的原因是因为我们都是对Vector进行操作的,所以对应的解决方法就是在操作Vector前锁住即可。

但是就算是锁住了,但是在遍历Vector的时候,有别的线程修改了Vector的长度,也是会有问题的。

在JDK5以后java推荐使用for-each(迭代器)来遍历集合,好处是简洁、数组索引的边界值只计算一次。

但是对于此问题就算使用迭代器也是会抛出异常。

要想真正解决这些问题,就需要我们在遍历前加锁,但是会导致效率低下。所以这个时候就需要我们的CopyOnWriteArrayList来解决问题。

CopyOnWriteArrayList介绍

一般来说我们认为CopyOnWriteArrayList(Set)是同步List(Set)的代替品。

  • CopyOnWrite和Vector(HashTable)比较

    • Vector和Hashtable最大的问题在于加锁的粒度大(直接在方法中使用Synchronized)
    • 而CopyOnWriteArrayList加锁粒度小(用各种的方式来实现线程安全(cas锁、volatile))
    • JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常
实现原理
  • 总览:

其是线程安全容器(相对于ArrayList),底层通过复制数组的方式来实现。

其在遍历的时候不用额外加锁,且不会抛出ConcurrentModificationException异常。

元素可以为null

基本结构
    /** 可重入锁对象 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
    private transient volatile Object[] array;

    /**
     * 得到数组
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * 设置数组
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 初始化CopyOnWriteArrayList相当于初始化数组
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

从基本结构来看,其底层就是数组,加锁就由ReentrantLock来实现。

常见方法
  • add()

首先我们来看一下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 {
            // 最后在finally中释放锁
            lock.unlock();
        }
    }

从源码阅读的过程中我们可以很清晰的看到,其核心思想就是复制出一个新数组然后在新数组上进行添加操作。然后将array指向新数组,最后解锁。

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

set方法和add方法的思路大体一致

  • 总结

    • 在修改时,会首先复制一个新数组,然后在新数组中进行修改,然后将array指向新数组
    • 写加锁,读不加锁
为什么遍历时不用调用者显示加锁

在上面减少和ArrayList的对比中我们提到,其有一点就是不会抛出异常,想要了解这个问题的答案还是要通过代码来进行分析

   // 1. 返回的迭代器是COWIterator
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }


    // 2. 迭代器的成员属性
    private final Object[] snapshot;
    private int cursor;

    // 3. 迭代器的构造方法
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    // 4. 迭代器的方法...
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    //.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组

通过代码我们可以很清楚的发现,其在使用迭代器遍历的时候,操作的数组都是原数组。

缺点

通过前面的分析我们可以知道其也存在一些严重的缺点或者问题:

  • 内存占用:因为在修改的时候,需要赋值一个新数组导致内存占用很大
  • 数据一致性问题:通过迭代的代码我们可以知道其只保证数据的最终一致性,并不保证数据的实时一致性。

最后

  • 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
  • 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
  • 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。

image

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值