Java同步容器总结

Posted htkj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java同步容器总结相关的知识,希望对你有一定的参考价值。

 《0》StringBuffer适用于多线程场景,StringBuilder适用于字符串拼接【堆栈封闭】

  `Vector`实现`List`接口,底层和`ArrayList`类似,但是`Vector`中的方法都是使用`synchronized`修饰,即进行了同步的措施。 但是,`Vector`并不是线程安全的。

  `Stack`也是一个同步容器,也是使用`synchronized`进行同步,继承与`Vector`,是数据结构中的,先进后出。

  `HashTable`和`HashMap`很相似,但`HashTable`进行了同步处理。

  `Collections`工具类提供了大量的方法,比如对集合的排序、查找等常用的操作。同时也通过了相关了方法创建同步容器类

如果在使用foreach或iterator进集合的遍历,尽量不要在操作的过程中进行remove等相关的更新操作。如果非要进行操作,则可以在遍历的过程中记录需要操作元素的序号,待遍历结束后方可进行操作,让这两个动作分开进行

?   同步容器中的方法主要采取synchronized进行同步,因此执行的性能会收到受到影响,并且同步容器并不一定能做到真正的线程安全。

《1》CopyOnWriteArrayList

?     ArrayList -> CopyOnWriteArrayList 

  CopyOnWriteArrayList相比于ArrayList是线程安全的,从字面意思理解,即为写操作时复制。CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

?     CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

   

CopyOnWtiteArrayList.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();

        }

    }

  

    缺点:由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc;

  不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList能做到最终一致性,但是还是没法满足实时性要求;

?    总结:CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用  因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。

    设计思想:读写分离,读和写分开;最终一致性。最终保证List的结果是对的;使用另外开辟空间的思路,来解决并发冲突

    CopyOnWriteArrayList并发的读,则分几种情况:

    如果写操作未完成,那么直接读取原数组的数据;

    如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;

    如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

《2》CopyOnWriteArraySet

?     HashSet -> CopyOnWriteArraySet

    CopyOnWriteArraySet底层实现是采用CopyOnWriteArrayList,适合比较小的集合,其中所有可变操作(add、set、remove等等)都是通过对底层数组进行一次新的复制来实现的,一般需要很大的开销。迭代器支持hasNext(), next()等不可变操作,不支持可变的remove操作;使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

《3》ConcurrentSkipListSet

  底层使用ConcurrentSkipListMap,TreeSet -> ConcurrentSkipListSet

  ConcurrentSkipListSet<E>和TreeSet一样,都是支持自然排序,并且可以在构造的时候定义Comparator<E>的比较器,该类的方法基本和TreeSet中方法一样(方法签名一样)

  和其他的Set集合一样,ConcurrentSkipListSet<E>都是基于Map集合的,ConcurrentSkipListMap便是它的底层实现

  在多线程的环境下,ConcurrentSkipListSet<E>中的contains、add、remove操作是安全的,多个线程可以安全地并发执行插入、移除和访问操作。但是对于批量操作addAll、removeAll、retainAll 和 containsAll并不能保证以原子方式执行。理由很简单,因为addAll、removeAll、retainAll底层调用的还是contains、add、remove的方法,在批量操作时,只能保证每一次的contains、add、remove的操作是原子性的(即在进行contains、add、remove三个操作时,不会被其他线程打断),而不能保证每一次批量的操作都不会被其他线程打断。因此,在addAll、removeAll、retainAll 和 containsAll操作时,需要添加额外的同步操作。

  此类不允许使用 null 元素,因为无法可靠地将 null 参数及返回值与不存在的元素区分开来。

《4》ConcurrentHashMap

?  HashMap -> ConcurrentHashMap,不允许null值,绝大部分使用Map都是读取操作,而且读操作大多数都是成功的,因此,ConcurrentHashMap针对读操作进行了大量的优化。在高并发的场景下,有很大的优势。

  因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。 HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

  HashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。

?  其实HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMapJDK1.7版本的核心思想。

《5》ConcurrentSkipListMap

?     TreeMap -> ConcurrentSkipListMap,内部使用``SkipList`结构实现的。跳表是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(log n)。

?     跳表(SkipList):使用“空间换时间”的算法,令链表的每个结点不仅记录next结点位置,还可以按照level层级分别记录后继第level个结点。

    concurrentHashMap与ConcurrentSkipListMap性能测试

?     在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。

?     但ConcurrentSkipListMap有几个ConcurrentHashMap不能比拟的优点:

  ConcurrentSkipListMap 的key是有序的,而ConcurrentHashMap是做不到的

  ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。

     在非多线程情况下,尽量使用TreeMap,此外,对于并发性较低的程序,可以使用Collections工具所提供的方法synchronizedSortMap,它是将TreeMap进行包装。对于高并发场景下,应使用`ConcurrentSkipListMap`提供更高的并发度。并且,如果在多线程环境下,需要对`Map`的键值进行排序时,也要尽量使用ConcurrentSkipListMap。

《6》安全共享策略总结

?   以下策略是通过线程安全策略中的不可变对象、线程封闭、同步容器以及并发容器相关知识总结而得:

  线程限制:一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

  共享只读:一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它

  线程安全对象:一个线程安全的对象或容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它

  被守护对象:被守护对象只能通过获取特定的锁来访问

 

以上是关于Java同步容器总结的主要内容,如果未能解决你的问题,请参考以下文章

java 容器类总结

Java学习笔记—多线程(同步容器和并发容器)

[Java 并发编程实战] 同步容器类潜在的问题(含实例代码)

[Java 并发编程实战] 同步容器类潜在的问题(含实例代码)

java-- java并发包总结

Java 多线程 8同步容器与并发容器