并发容器概览
- ConcurrentHashMap:线程安全的HashMap
- CopyOnWriteArrayList:线程安全的List
- BlockingQueue:这是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道
- ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现,可以看做一个线程安全的LinkedList
- ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找。
古老和过时的同步容器
- Vector和Hashtable:可以理解为各方法都加了使用synchronized修饰的ArrayList和HashMap。
- HashMap和ArrayList:使用Collections.synchronizedList(new ArrayList<E>())和Collections.synchronizedMap(new HashMap<E>())进行包装
- 取代同步的HashMap和同步的ArrayList:ConcurrentHashMap和CopyOnWriteArrayList。
ConcurrentHashMap(重点、面试常考)
-
Map简介
-
为什么需要ConcurrentHashMap
- 为什么不用Collections.synchronizedMap():相当于对每个方法进行synchronized包装,但synchronized方法在并发量高的时候性能并不理想。
-
为什么HashMap是线程不安全的
- 同时put碰撞导致数据丢失
- 同时put扩容导致数据丢失
- 死循环造成的CPU100%(兴趣了解)
-
HashMap的分析
- 1.7的HashMap采用的是数组加链表;1.8采用的是链表加数组(红黑树),当链表的长度超过8之后使用红黑树
- 红黑树:对二叉查找树BST的一种平衡策略,会自动平衡,防止极端不平衡从而影响查找效率的情况发生。
- HashMap关于并发的特点:非线程安全;迭代时不允许修改内容;只读的并发是安全的;必要的情况下可以用Collections.synchronizedMap(new HashMap())将HashMap使用到并发环境中。
- 1.7的HashMap采用的是数组加链表;1.8采用的是链表加数组(红黑树),当链表的长度超过8之后使用红黑树
-
JDK1.7的ConcurrentHashMap
- 1.7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
- 每个segment独立上ReentrantLock锁,每个segment之间互不影响,提高了并发效率。
- ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的。
-
JDK1.8的ConcurrentHashMap
- 底层与1,8的HashMap类似,都是数组+链表/红黑树,但每一个数组的节点都是同步的。
- put
- 判断key value不为空
- 计算hash值
- 根据对应位置节点的类型,来赋值,或者helpTransfer,或者增长链表,或者给红黑树增加节点
- 检查满足阈值就“红黑树化”
- 返回oldVal
- get
- 计算hash值
- 找到对应的位置,根据情况进行:直接取值;红黑树里找值;遍历链表取值;
- 返回找到的结果。
-
为什么要修改结构
- 数据结构
由原来的默认16个线程独立变成了每一个数组节点都是独立的,提高的并行度 - Hash碰撞
- 红黑树比单纯的拉链法效率更高。
- 保证并发安全
- 1.7采用分段锁segment,继承自ReentrantLock;1.8采用CAS + Synchronized
- 查询复杂度
- 1.7复杂度O(n),1.8复杂度O(log n)
- 数据结构
-
为什么ConcurrentHashMap也不是线程安全的
ConcurrentHashMap中有Map中的put方法,而这个方法不是线程安全的;替代的方法是replace方法,replace方法实现线程安全的原理是对变量进行原子更新
CopyOnWriteArrayList
-
代替Vector和SynchronizedList就和ConcurrentHashMap代替SynchronizedMap的原因一样,Vector和SynchronizedList的锁粒度太大,并发效率相对较低,并且迭代时无法编辑。
-
适用场景:
- 读操作可以尽可能的快,写操作慢一点没有太大关系
- 读多写少:黑名单,每日更新;监听器:迭代操作远多于修改操作。
-
读写规则:
- 读写锁:读读共享、其他都互斥
- CopyOnWriteArrayList相当于读写锁规则升级:读取是完全不用加锁的,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。
-
CopyOnWriteArrayList实现原理
- CopyOnWrite的含义:在要对一块内存进行修改时,不直接修改这个内存的值,而是将这个内存中的东西copy一份做修改,然后把之前的指针指到新的内存。
- 创建新副本、读写分离
- 旧内存的值是不可变的,所以支持同步读。
- 迭代的时候用的是旧的地址的值,即使迭代过程中数组发生改变,迭代器也是原来的那个。数据可能过时。
-
CopyOnWriteArrayList缺点
- 数据一致性问题:CopyOnWrite容器只能保证数据最终一致性,不能保证数据的实时一致性。如果希望写入的数据马上能读到,就不要使用CopyOnWrite容器。
- 内存占用问题:因为底层采用了复制机制,所以在写的时候,内存中会同时驻扎两个对象的内存。
并发队列Queue(阻塞队列、非阻塞队列)
-
为什么要使用队列
- 用队列可以在线程间传递数据:生产者消费者模式
- 线程安全问题由队列来考虑,降低编码难度
-
各并发队列关系图
-
BlockingQueue阻塞队列
- 什么是阻塞队列
- 阻塞队列是具有阻塞功能的队列。通常阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的
- 阻塞功能:最有特色的两个带有阻塞功能的方法
- take方法:获取并溢出队列头结点,如果执行take方法的时候,队列里无数据则阻塞,直到队列中有数据。
- put方法:插入数据,如果队列已满就阻塞,直到队列里有空闲空间。
- 是否有界:可以通过参数控制队列的容量,无界为Integer.MAX_VALUE。
- 阻塞队列和线程池的关系:阻塞队列时线程池的重要组成部分
- 主要方法
- put,take
- add,remove,element
- offer,poll,peek
- ArrayBlockingQueue
- 有界,可指定容量
- 公平:可以通过指定是否保证公平。公平则优先处理等待时间最长的线程。这可能会带来一定的性能损耗。
- LinkedBlockingQueue
- 无界,容量Integer.MAX_VALUE
- 内部结构:Node、两把锁
- PriorityBlockingQueue
- 支持优先级,自然顺序,不是先进先出。
- 无界队列
- PriorityQueue的线程安全版本
- SynchronousQueue
- 容量为0,因为SynchronousQueue不需要持有元素,他所做的就是直接传递(direct handoff),所以效率很高。
- SynchronousQueue没有peek函数,因为容量为0。同理没有iterate相关方法。
- 是一个极好的用来直接传递的并发数据结构。
- Executors.newCashedThreadPool()使用的阻塞队列
- DelayQueue
- 延迟队列,根据延迟时间排序。元素需要实现Delayed接口,规定排序规则
- 什么是阻塞队列
-
非阻塞队列ConcurrentLinkedQueue
- 使用链表作为数据结构,使用CAS非阻塞算法来实现线程安全
-
如何选择适合的队列
- 边界
- 空间
- 吞吐量