什么是并发容器
开发中我们常用的数据结构有四大类别:List、Set、Queue、Map,这四大类中常用的是ArrayList、LinkedList、HashMap等,这些容器都是非线程安全的。在并发情况下,会有各种问题,如果想要保证线程安全,那么需要进行加锁操作,使用起来非常不方便,这种情况下,也就开发出了专门在并发情况下使用的容器。
并发容器概览
- ConcurrentHashMap:线程安全的HashMap
- CopyOnwriteArrayList:线程安全的List
- BlockingQueue:这是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道
- ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现,可以看做一个线程安全的LinkedList
- ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找
过时的并发容器
- Vector和HashTable
Vector和HashTable只是在原有数据结构上(ArrayList和HashMap的基础上加了synchronized关键字,效率很低)- HashMap和ArrayList
HashMap和ArrayList虽然不是线程安全的,但是可以用Collections.synchronizedList(new ArrayList())和Collections.synchronizedMap(new HashMap<K,V>())使其变成线程安全的,不过效率也比较低。
为什么HashMap是线程不安全的?
- 同时put碰撞导致数据丢失
在多线程同时操作HashMap时,如果计算出的hashcode一样,就只会保留一个,另一个数据就会丢失- 同时put扩容导致数据丢失
在多线程同时操作HashMap时,如果同时put,同时扩容,也只有一个会保留下来- 死循环造成的CPU100%
主要存在JDK7及以前
原因:在多个线程同时扩容的时候,会造成链表的死循环,也就是你指向我,我指向你的情况
JDK1.7的ConcurrentHashMap实现和分析
- java7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构都和HashMap类似,仍然是数组和链表组成的拉链法
- 每个segment独立上ReentrantLock锁,每个segement之间互不影响,因此提高了并发效率
- ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的。
ConcurrentHashMap
1.判断key value不为空
2.计算hash值
3.根据对应位置节点的类型,来赋值,或者helptransfer,或者增长链表,或者给红黑树增加节点
4.检查满足阀值就“红黑树化”
5.返回oldVal1.计算hash值
2.找到对应的位置,根据情况进行:
3.直接取值
4.红黑树里找值
5.遍历链表取值
6.返回找到的结果
CopyOnWriteArrayList
- 代替Vector和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因一样
- Vector和SynchronizedList的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑
- Copy-On-Write并发容器还包括CopyOnWriteArraySet,用来代替同步Set
CopyOnWriteArrayList适用场景
- 读操作可以尽可能地快,而写即使慢一些也没有太大关系
- 读多写少:例如黑名单,每日更新;监听器:迭代操作远多于修改操作
CopyOnWriteArrayList读写规则
- 回顾读写锁:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
- 读写锁规则的升级:读取是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待,也就是说读写可以兼容,只有写写才会等待
CopyOnWriteArrayList实现原理
- 创建新副本、读写分离
- 写的时候会创建一个新的容器,读的时候会在旧的容器里读,因此读写互不相干,哪怕写入了新的值,但是读取的时候还是会读最开始的那些数据,等写完之后,再将老容器的数据指向新的容器,旧容器就作废了,等于做了新旧数据的替换
CopyOnWriteArrayList的缺点
- 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上能读到,就不能使用CopyOnWrite容器。
- 内存占用问题:因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,所以比较占用内存。
源码分析
源码里可以看到,就是一个array,里面应用了ReentrantLock来保证线程安全
并发队列Queue:阻塞队列
并发队列关系图
阻塞队列BlockingQueue
什么是阻塞队列?
1.阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是具有阻塞功能。
2.通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的。
- take()方法:获取并移除队列的头结点,一旦如果执行take的时候,队列里无数据,则阻塞,知道队列里有数据
- put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间
- 是否有界(容量有多大):这是一个非常重要的属性,无界队列意味着里面可以容纳非常多(Integer.MAX_VALUE,约为2的31次方,是一个非常大的数值,可以近似认为是一个无限容量)
- 阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分
- put ,take:
put:添加数据,如果队列满了则会阻塞
take:和put类似,只不过是取值- add,remove,element:如果队列满了,无论是添加还是取出值,都会抛异常
- offer:放入值,并将结果返回,为boolean类型
poll:取出并将该值删除,如果没有就返回null
peek:取出该值,但是不删除ArrayBlockingQueue
- 有界
- 指定容量
- 公平:还可以指定是否需要保证公平,如果想保证公平的话,那么等待了最长时间的线程会被优先处理,不过这会同时带来一定的性能损耗
LinkedBlockingQueue
- 无界
- 容量Integer.MAX_VALUE
- 内部结构:Node、两把锁(take、put都加了锁)。
PriorityBlockingQueue
- 支持优先级
- 自然顺序(而不是先进先出)
- 无界队列
- PriorityQueue的线程安全版本
proirityBlockingQueue是一个无界队列,他没有限制,在内存允许的情况下可以无限添加元素;他又是具有优先级的队列,是通过构造函数传入的对象来判断,传入的对象必须实现comparable接口。SynchronousQueue
- 他的容量为0
- 需要注意的是,SynchronousQueue的容量不是1而是0,以为SynchronousQueue不需要去持有元素,他所作的就是直接传递
- 效率很高
DelayQueue
- 延迟队列,根据延迟时间排序
- 元素需要实现Delayed接口,规定排序规则
DelayQueue内部封装了一个PriorityQueue,他会根据time的先后时间排序(time小的排在前面);DelayQueue也是一个无边界队列
非阻塞并发队列
-并发包中的非阻塞队列只有ConcurrentLinkedQueue这一种,顾名思义ConcurrentLinkedQueue是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全(不具备阻塞功能),适合用在对性能要求较高的并发场景。用的相对比较少一些
- 看源码的offer方法的CAS思想,内有p.casNext方法,用了UNSAFE.compareAndSwapObject
并发容器总结
- java.util.concurrent包提供的容器,分为3类:Concurrent*、CopyOnWrite*、Blocking*
- Concurrent的特点是大部分通过CAS实现并发,而CopyOnWrite则是通过复制一份原数据来实现的,Blocking通过AQS实现的