【Java并发解析】并发容器解析
文章目录
并发容器概览
常用的并发容器
重点的主要是ConcurrentHashMap
, CopyOnWriteArrayList
, 和接口BlockingQueue
淘汰的并发容器
-
Vector
:性能不好,主要是全部都是synchronized
保护起来的方法,并发性不好。 -
Hashtable
: 线程安全的hashmap,同时是性能不好,sychronized修饰。 -
Collections.synchronizedList(new ArrayList<E>())
和Collections.synchronizedMap(new HashMap<K, V>())
: 把synchronized加在了代码块上。
锁的粒度太大。
ConcurrentHashMap
我们首先从HashMap的源码开始分析,map在Java里面是一个很大的家族体系。
-
常用的家族:
HashMap
: 朴实无华的字典结构LinkedHashMap
: 可以保留插入的顺序,可以按照顺序查找。TreeMap
: 实现了排序接口,可以按照key的顺序进行一定的处理。 -
为什么Hashmap是线程不安全的:
- 同时put碰撞导致数据丢失。
- 同时扩容导致数据调式
- 可能导致死循环。
HashMap
采用拉链法处理哈希碰撞,在1.8中,当拉链法的长度到达一定的程度时候(默认为8),会变成红黑树。用BST可以提高查找的速度,用红黑树可以自动平衡节点的防止极端不平衡影响查找的效率。默认负载因此0.75。
-
为什么要采用红黑树的形式:
默认是链表因为链表需要的存储空间小,并且在8以上才需要转换,这种概率极低,千万分之几。保证在极端情况下可以保证查询速度。
ConcurrentHashMap
采用多个segment的思路,每个segment都有一个独立的ReentrantLock锁,彼此互不影响,提高了并发效率。默认是16个的segment,初始化之后不可以扩容。
第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个以上,如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64了的话,在会将该节点的链表转换成树。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。
在任何一个构造方法中,都没有对存储Map元素Node的table变量进行初始化。而是在第一次put操作的时候在进行初始化。
在1.8中,采用的是类似与HashMap的方法,并且是分为了很多个node,因此每一个节点都具有并发性。并且使用了CAS的方法。
核心方法:
源码解析putVal
这个方法在增长链表或者给红黑树加节点这个环节还是用了synchronized。
还可以使用一些线程安全的方法如map.replace(key, oldval, newval)
这个可以实现类似于CAS的操作,返回一个布尔逻辑值告诉我们操作是否如愿。map.putIfAbsent(key, val)
如果当前key不存在,就放入,否则取出。
并发队列
并发队列概述
- 为什么使用:可以在线程之间传递数据;不用操心安全性
- 线程安全的队列:
阻塞队列BlockingQueue
- put,take:会阻塞
- add,remove,element:如果无法实现,会返回异常
- offer, poll, peekL:如果失败会返回布尔值。
源码分析:LinkedBlockingQueue分别使用了putLock和takeLock,利用singnal进行通信。
非阻塞队列ConcurrentLinkedQueue
采用了CAS的方法实现并发的线程安全。
CopyOnWriteArrayList
-
构造:
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});
-
适用场景:读操作尽可能的快,即使写慢一些也无所谓。读多写少。
-
读写规则:**只有写写之间存在互斥。**实际上是修改了备份。因此存在一个小的问题,在迭代中修改的东西没法立刻体现。迭代器能的得到的数据,取决于创建的时间,而不是迭代的时间。
-
原理:创建了新的副本,读写分离的思想。
-
缺点:数据一致性存在问题;内存占用问题。
源码分析
使用了Reentrantlock
进行add方法