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 并发编程实战] 同步容器类潜在的问题(含实例代码)