• JAVA提高二十:CopyOnWriteArrayList&CopyOnWriteArraySet&ConcurrentHashMap介绍


    前面我们将java集合类的大部分类都进行了深入分析,但我们会发现一个共性问题就是并发的问题,那么如何解决呢?我们前面基本都是通过Collections的一个工具类来进行的解决,但实际大部分使用中人们普遍会使用并发的容器,在JDK1.5之后,针对基于散列的Map,提供了新的ConcurrentHashMap,针对迭代需求的list,提供了CopyOnWriteList.。因此这里我进行下简单介绍和分析,具体的原理实现将在并发学习中进行详细的介绍。

    一、集合总结

    我们前面学习了很多集合类,这里我们做一个简单的总结,先整体了解类的关系,如下图所示:

    大部分我们应该都有过介绍了,下面我们回顾一些重点知识:

    关  注  点 结      论
    ArrayList是否允许空 允许
    ArrayList是否允许重复数据 允许
    ArrayList是否有序 有序
    ArrayList是否线程安全 非线程安全
    关  注  点 结      论
    LinkedList是否允许空 允许
    LinkedList是否允许重复数据 允许
    LinkedList是否有序 有序
    LinkedList是否线程安全 非线程安全
    关  注  点 结      论
    HashMap是否允许空 Key和Value都允许为空
    HashMap是否允许重复数据 Key重复会覆盖、Value允许重复
    HashMap是否有序 无序,特别说明这个无序指的是遍历HashMap的时候,得到的元素的顺序基本不可能是put的顺序
    HashMap是否线程安全 非线程安全
    关  注  点 结      论
    LinkedHashMap是否允许键值对为空 Key和Value都允许空
    LinkedHashMap是否允许重复数据 Key重复会覆盖、Value允许重复
    LinkedHashMap是否有序 有序
    LinkedHashMap是否线程安全 非线程安全

    二、CopyOnWriteArrayList&CopyOnWriteArraySet&ConcurrentHashMap

    1.引言
      在多线程的环境中,如果想要使用容器类,就需要注意所使用的容器类是否是线程安全的。在最早开始,人们一般都在使用同步容器(Vector,HashTable),其基本的原理,就是针对容器的每一个操作,都添加synchronized来进行同步,此种方式尽管简单,但是其性能是非常地下的,所以现在已经不怎么使用了。人们普遍会使用并发的容器,在JDK1.5之后,针对基于散列的Map,提供了新的ConcurrentHashMap,针对迭代需求的list,提供了CopyOnWriteList.
    2.ConcurrentHashMap
      ConcurrentHashMap使用了一种分段锁的策略,使得map可以被多个读写线程并行的访问。基本可以认为是将map的key值范围分为多个段,这样多个线程访问的时候,他们需要访问的key值在不同的段,所以可以互相不干扰,
    使用不同的锁对象来进行并发操作。
      ConcurrentHashMap在使用迭代器遍历的时候,不会报ConcurrentModificationException,提供“弱一致性”。在遍历迭代的时候,也会反应出在迭代器创建之后的数据修改。
    应用场景
    针对一般的有并发需求的map,都应该使用ConcurrentHashMap. 它的性能优于Hashtable和synchronizedMap。
      缺点
    1.不是强一致性 
      由于是采用的分段锁策略,所以一些数据不能保证强一致性。比如针对容器的size方法,由于线程A只是获得了自己的分段锁,它不能保证其他线程对容器的修改,所以此时线程A可能使用size,会得到不稳定数据。这种情况下,是对同步性能的一些折衷。如果业务需求必须满足强一致性,才会需要对整个Map进行锁操作。并发容器的弱一致性的概念背景,是在高并发情况下,容器的size和isEmpty之类的方法,用处不大,所以可以忍受数据不一致性。
    3.CopyOnWrite容器
      在JDK1.5之后,java.util.concurrent引入了两个CopyOnWrite容器,分别是CopyOnWriteArrayList, CopyOnWriteArraySet.
      顾名思义,CopyOnWrite就是在write操作之前,对集合进行Copy,针对容器的任意改操作(add,set,remove之类),都是在容器的副本上进行的。并且在修改完之后,将原容器的引用指向修改后的副本。
    如果线程A得到容器list1的iterator之后,线程B对容器list1加入了新的元素,由于线程A获得list1的iterator时候在线程B对list1进行修改前,所以线程A是看不到线程B对list1进行的任何修改。
    具体到源码,看一下add操作

     /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return {@code true} (as specified by {@link Collection#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 {
                lock.unlock();
            }
        }

    可以发现,写操作是会有个锁lock.lock(),这保证了多线程写操作之间的同步。之后使用Arrays.copyOf来进行数组拷贝,在修改完成后,setArray(newElements)将原来的数组引用指向新的数组。
      应用场景
    经常用在读多写少的场景,比如EventListener的添加,网站的category列表等偶尔修改,但是需要大量读取的情景。
      缺点
    1.数据一致性的问题。  
      因为读操作没有用到并发控制,所以可能某个线程读到的数据不是实时数据。
    2.内存占用问题。
      因为写操作会进行数据拷贝,并且旧有的数据引用也可能被其他线程占有一段时间,这样针对数据比较大的情况,可能会占用相当大的内存。并且由于每次写操作都会占用额外的内存,最后进行的GC时间也可能相应的增加。

    最后,集合的介绍知识到此结束,感谢博客园的各位园友们,下面的学习将进入IO/Nio的学习。

    参考资料:

    http://wiki.jikexueyuan.com/project/java-collection/linkedhashset.html
    https://www.cnblogs.com/skywang12345
    http://blog.csdn.net/column/details/java-collections.html
    https://www.cnblogs.com/xrq730
    http://www.cnblogs.com/xiaoxi/category/929860.html

  • 相关阅读:
    javascript封装与多态的体现
    js事件处理、事件对象
    对js中this的一点点理解
    js中的位运算
    css中的各种单位简述以及ios10下safari禁止缩放的问题
    js变量提升
    Web Worker javascript多线程编程(一)
    MongoDB数据库备份和恢复
    什么是Git及Git的诞生
    MongoDB安装过程
  • 原文地址:https://www.cnblogs.com/pony1223/p/8004721.html
Copyright © 2020-2023  润新知