CopyOnWriteArrayList你都不知道,怎么拿offer?

前言

只有光头才能变强

cow

前一阵子写过一篇COW(Copy On Write)文章,结果阅读量很低啊...COW奶牛!Copy On Write机制了解一下java

可能你们对这个技术比较陌生吧,但这项技术是挺多应用场景的。除了上文所说的Linux、文件系统外,其实在Java也有其身影。git

你们对线程安全容器可能最熟悉的就是ConcurrentHashMap了,由于这个容器常常会在面试的时候考查。github

好比说,一个常见的面试场景:面试

  • 面试官问:“HashMap是线程安全的吗?若是HashMap线程不安全的话,那有没有安全的Map容器”
  • 3y:“线程安全的Map有两个,一个是Hashtable,一个是ConcurrentHashMap”
  • 面试官继续问:“那Hashtable和ConcurrentHashMap有什么区别啊?”
  • 3y:“balabalabalabalabalabala"
  • 面试官:”ok,ok,ok,看你Java基础挺不错的呀“

那若是有这样的面试呢?编程

  • 面试官问:“ArrayList是线程安全的吗?若是ArrayList线程不安全的话,那有没有安全的相似ArrayList的容器”
  • 3y:“线程安全的ArrayList咱们可使用Vector,或者说咱们可使用Collections下的方法来包装一下”
  • 面试官继续问:“嗯,我相信你也知道Vector是一个比较老的容器了,还有没有其余的呢?”
  • 3y:“Emmmm,这个...“
  • 面试官提示:“就好比JUC中有ConcurrentHashMap,那JUC中有相似"ArrayList"的线程安全容器类吗?“
  • 3y:“Emmmm,这个...“
  • 面试官:”ok,ok,ok,今天的面试时间也差很少了,你回去等通知吧。“

今天主要讲解的是CopyOnWriteArrayList~数组

本文力求简单讲清每一个知识点,但愿你们看完能有所收获安全

1、Vector和SynchronizedList

1.1回顾线程安全的Vector和SynchronizedList

咱们知道ArrayList是用于替代Vector的,Vector是线程安全的容器。由于它几乎在每一个方法声明处都加了synchronized关键字来使容器安全。服务器

Vector实现

若是使用Collections.synchronizedList(new ArrayList())来使ArrayList变成是线程安全的话,也是几乎都是每一个方法都加上synchronized关键字的,只不过它不是加在方法的声明处,而是方法的内部多线程

Collections.synchronizedList()的实现

1.2Vector和SynchronizedList可能会出现的问题

在讲解CopyOnWrite容器以前,咱们仍是先来看一下线程安全容器的一些可能没有注意到的地方~并发

下面咱们直接来看一下这段代码:

// 获得Vector最后一个元素
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }

    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }

以咱们第一反应来分析一下上面两个方法:在多线程环境下,是否有问题

  • 咱们能够知道的是Vector的size()和get()以及remove()都被synchronized修饰的。

答案:从调用者的角度是有问题

咱们能够写段代码测试一下:

import java.util.Vector;

public class UnsafeVectorHelpers {


    public static void main(String[] args) {

        // 初始化Vector
        Vector<String> vector = new Vector();
        vector.add("关注公众号");
        vector.add("Java3y");
        vector.add("买Linux可到我下面的连接,享受最低价");
        vector.add("给3y加鸡腿");

        new Thread(() -> getLast(vector)).start();
        new Thread(() -> deleteLast(vector)).start();
        new Thread(() -> getLast(vector)).start();
        new Thread(() -> deleteLast(vector)).start();
    }

    // 获得Vector最后一个元素
    public static Object getLast(Vector list) {
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }

    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

能够发现的是,有可能会抛出异常的:

代码抛出异常

缘由也很简单,咱们照着流程走一下就行了:

  • 线程A执行getLast()方法,线程B执行deleteLast()方法
  • 线程A执行int lastIndex = list.size() - 1;获得lastIndex的值是3。同时,线程B执行int lastIndex = list.size() - 1;获得的lastIndex的值是3
  • 此时线程B先获得CPU执行权,执行list.remove(lastIndex)将下标为3的元素删除了
  • 接着线程A获得CPU执行权,执行list.get(lastIndex);,发现已经没有下标为3的元素,抛出异常了.

交替执行致使异常发生

出现这个问题的缘由也很简单:

  • getLast()deleteLast()这两个方法并非原子性的,即便他们内部的每一步操做是原子性的(被Synchronize修饰就能够实现原子性),可是内部之间仍是能够交替执行。

    • 这里的意思就是:size()和get()以及remove()都是原子性的,可是若是并发执行getLast()deleteLast(),方法里面的size()和get()以及remove()是能够交替执行的。

要解决上面这种状况也很简单,由于咱们都是对Vector进行操做的,只要操做Vector前把它锁住就没毛病了

因此咱们能够改为这样子:

// 获得Vector最后一个元素
    public static Object getLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            return list.get(lastIndex);
        }
    }
    // 删除Vector最后一个元素
    public static void deleteLast(Vector list) {
        synchronized (list) {
            int lastIndex = list.size() - 1;
            list.remove(lastIndex);
        }
    }
ps:若是有人去测试一下,发现会抛出异常java.lang.ArrayIndexOutOfBoundsException: -1,这是 没有检查角标的异常,不是并发致使的问题。

通过上面的例子咱们能够看看下面的代码:

public static void main(String[] args) {

        // 初始化Vector
        Vector<String> vector = new Vector();
        vector.add("关注公众号");
        vector.add("Java3y");
        vector.add("买Linux可到我下面的连接,享受最低价");
        vector.add("给3y加鸡腿");

        // 遍历Vector
        for (int i = 0; i < vector.size(); i++) {

            // 好比在这执行vector.clear();
            //new Thread(() -> vector.clear()).start();

            System.out.println(vector.get(i));
        }
    }

一样地:若是在遍历Vector的时候,有别的线程修改了Vector的长度,那仍是会有问题

  • 线程A遍历Vector,执行vector.size()时,发现Vector的长度为5
  • 此时颇有可能存在线程B对Vector进行clear()操做
  • 随后线程A执行vector.get(i)时,抛出异常

Vector遍历抛出异常

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

若是使用for-each(迭代器)来作上面的操做,会抛出ConcurrentModificationException异常

迭代器遍历会抛出ConcurrentModificationException

SynchronizedList在使用迭代器遍历的时候一样会有问题的,源码已经提醒咱们要手动加锁了。

SynchronizedList在遍历的时候一样会有问题的

若是想要完美解决上面所讲的问题,咱们能够在遍历前加锁

// 遍历Vector
         synchronized (vector) {
            for (int i = 0; i < vector.size(); i++) {
                vector.get(i);
            }
        }

有经验的同窗就能够知道:哇,遍历一下容器都要我加上锁,这这这不是要慢死了吗.的确是挺慢的..

因此咱们的CopyOnWriteArrayList就登场了!

2、CopyOnWriteArrayList(Set)介绍

通常来讲,咱们会认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArraySet是同步Set的替代品。

不管是Hashtable-->ConcurrentHashMap,仍是说Vector-->CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的问题

  • Hashtable、Vector加锁的粒度大(直接在方法声明处使用synchronized)
  • ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小(用各类的方式来实现线程安全,好比咱们知道的ConcurrentHashMap用了cas锁、volatile等方式来实现线程安全..)
  • JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常

因此通常来讲,咱们都会使用JUC包下给咱们提供的线程安全容器,而不是使用老一代的线程安全容器。

下面咱们来看看CopyOnWriteArrayList是怎么实现的,为何使用迭代器遍历的时候就不用额外加锁,也不会抛出ConcurrentModificationException异常。

2.1CopyOnWriteArrayList实现原理

咱们仍是先来回顾一下COW:

若是有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取 相同的指针指向相同的资源,直到某个调用者 试图修改资源的内容时,系统才会 真正复制一份专用副本(private copy)给该调用者,而其余调用者所见到的最初的资源仍然保持不变。 优势是若是调用者 没有修改该资源,就不会有副本(private copy)被创建,所以多个调用者只是读取操做时能够 共享同一份资源

参考自维基百科:https://zh.wikipedia.org/wiki/%E5%AF%AB%E5%85%A5%E6%99%82%E8%A4%87%E8%A3%BD

以前写博客的时候,若是是要看源码,通常会翻译一下源码的注释并用图贴在文章上的。Emmm,发现阅读体验并非很好,因此我这里就 直接归纳一下源码注释说了什么吧。另外,若是使用IDEA的话,能够下一个插件 Translation(免费好用).

Translation插件

Translation插件


归纳一下CopyOnWriteArrayList源码注释介绍了什么:

  • CopyOnWriteArrayList是线程安全容器(相对于ArrayList),底层经过复制数组的方式来实现。
  • CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,而且遍历的时候就不用额外加锁
  • 元素能够为null

2.1.1看一下CopyOnWriteArrayList基本的结构

/** 可重入锁对象 */
    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]);
    }

看起来挺简单的,CopyOnWriteArrayList底层就是数组,加锁就交由ReentrantLock来完成。

2.1.2常见方法的实现

根据上面的分析咱们知道若是遍历Vector/SynchronizedList是须要本身手动加锁的。

CopyOnWriteArrayList使用迭代器遍历时不须要显示加锁,看看add()、clear()、remove()get()方法的实现可能就有点眉目了。

首先咱们能够看看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;
            
            // 将volatile Object[] array 的指向替换成新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

经过代码咱们能够知道:在添加的时候就上锁,并复制一个新数组,增长操做在新数组上完成,将array指向到新数组中,最后解锁。

再来看看size()方法:

public int size() {

        // 直接获得array数组的长度
        return getArray().length;
    }

再来看看get()方法:

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

    final Object[] getArray() {
        return 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;
            
            // 将array引用指向新数组
            setArray(newElements);
        } else {
            // Not quite a no-op; enssures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

对于remove()、clear()set()和add()是相似的,这里我就再也不贴出代码了。

总结:

  • 在修改时,复制出一个新数组,修改的操做在新数组中完成,最后将新数组交由array变量指向
  • 写加锁,读不加锁

2.1.3剖析为何遍历时不用调用者显式加锁

经常使用的方法实现咱们已经基本了解了,但仍是不知道为啥可以在容器遍历的时候对其进行修改而不抛出异常。因此,来看一下他的迭代器吧:

// 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数组

到这里,咱们应该就能够想明白了!CopyOnWriteArrayList在使用迭代器遍历的时候,操做的都是原数组

一张图来解析COW容器

2.1.4CopyOnWriteArrayList缺点

看了上面的实现源码,咱们应该也大概能分析出CopyOnWriteArrayList的缺点了。

  • 内存占用:若是CopyOnWriteArrayList常常要增删改里面的数据,常常要执行add()、set()、remove()的话,那是比较耗费内存的。

    • 由于咱们知道每次add()、set()、remove()这些增删改操做都要复制一个数组出来。
  • 数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性

    • 从上面的例子也能够看出来,好比线程A在迭代CopyOnWriteArrayList容器的数据。线程B在线程A迭代的间隙中将CopyOnWriteArrayList部分的数据修改了(已经调用setArray()了)。可是线程A迭代出来的是原有的数据。

2.1.5CopyOnWriteSet

CopyOnWriteArraySet的原理就是CopyOnWriteArrayList。

private final CopyOnWriteArrayList<E> al;

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

3、最后

如今临近双十一买阿里云服务器就特别省钱!以前我买学生机也要9.8块钱一个月,如今最低价只须要8.3一个月!

若是有要买服务器的同窗可经过个人连接直接享受最低价https://m.aliyun.com/act/team1111/#/share?params=N.FF7yxCciiM.pfn5xpli


阅读这篇文章可能须要对Java容器和多线程有必定的了解。若是对这些知识还不太了解的同窗们可看我以前写过的文章哦~

若是你们有更好的理解方式或者文章有错误的地方还请你们不吝在评论区留言,你们互相学习交流~~~

参考资料:

扩展阅读:

一个 坚持原创的Java技术公众号:Java3y,欢迎你们关注

3y全部的原创文章:

相关文章
相关标签/搜索