概要
jdk5引入 java.util.concurrent.*下的线程安全集合,里面包含关键字:BlockingQueue、CopyOnWrite、Concurrent;
- Blocking大部分实现基于锁,并提供等待性方法;
- CopyOnWrite 之类容器修改开销相对较重;
- Concurrent类型的容器:
- 基于lock-free 再常见的多线程访问场景,一般可以提供较高吞吐量;
- 往往提供了较低的遍历一致性,也就说当迭代器遍历时,修改了容器中的元素,而迭代器继续遍历;
- 读取的性能具有一定的不确定性;
线程安全控制的三个级别
- JVM级别:通常以CAS指令形式,是一种低级别的、细粒度的技术;
- 低级使用程序类——锁定和原子类,使用CSA作为并发原语,ReentrantLock类提供与synchronized原语相同的所动和内存语义;
- 高级使用程序类:信号、互斥、屏障、交换程序等;
- 线程安全集合
- 遗留的线程安全集合,如Hashtable
- Conllection 里的一系列以synchronized开头的方法,可以把非线程安全的集合包装成线程安全的集合;
- 体现了设计模式中的装饰器模式;
- juc下的线程安全集合
- CopyOnWrite 开头的集合,采用了写入时拷贝的思想,来提高并发度;
- Concurrent 支持并发(线程安全的集合);
- Blocking支持阻塞操作;
- CopyOnWriteArrayList->Vector
支持多线程并发读取,支持单线程写入;
读取方法不加锁,进行写入操作时,将原有数组复制一份,然后修改操作在新数组,等修改操作完了之后,将新数组替换旧数组;
把读写操作分开,读不加锁,写加锁,用空间来换取了读不加锁;
出现该集合的原因时由于Concurrent下的类的弱一致性迭代器,也就是当使用Concurrent下的集合返回的迭代器进行遍历迭代时,如果元素被修改则会抛出ConcurrentModificationException异常,且这个异常是不可控的。除此之外还有CopyWriteArraySet;
- ConcurrentHashMap->Hashtable
Hashtable是锁住了整个map集合,而ConcurrentHashMap 只会锁住map集合中的一个桶
1.7 之前
数组(Segment)+数组(hashEntry)+链表(HashEntry)
分段锁
- 首先是一个segment的数组,每个segment的元素又是一个Node数组,Node数组里又存放着链表;
- 锁定以segment为单位;
- 初始化,不是懒惰初始化,先把16个segment创建好,这个容量指定后就不能改变了;
- put 先找到segment,调用segment.put
- 懒惰初始化HashEntry数组(除了segment[0]除外);
- segment本身实现了可入锁,在put操作时会用lock加锁
- 元素添加至链表头;
- get 无锁操作,仅需要保证可见性,扩容过程中,get先发生就从旧表中读取,get发生后就从新表中读取;
- 扩容 发生在put方法内,因此是提前加了锁的;
- size 计算元素个数前,先不加锁多次计算,前后两次结果如一样,认为个数正确,计算超过3次将所有segment锁住,重新计算个数返回;
1.8 中
Hashtable 是锁住了整个map集合,而ConcurrentHashmap只会锁住map集合的一个桶,根据桶的多少,可以进一步提高并发度,只要读写操作落在不同的桶里,操作就可以并行执行;
- 数组(Node) + 链表/红黑树
- 初始化数组时,使用cas来保证并发安全性,懒惰初始化;
- 当容量小于64首先尝试进行扩容,当超过这个容量并且链表大于8,会将链表树化,树化过程中会锁住链表头;
- put 操作会锁住链表头,新加的元素放入链表尾部;
- get 操作不需要加锁,仅需要用cas保证元素的可见性;
- 扩容以链表为单位扩容,当扩容时有多个线程来同时访问,这些线程会协助扩容;
- size 元素个数保存在baseCount中,并发时的个数变动保存在CounterCell[] ,最后统计数量累加即可;
- ConcurrentSkipListMap
类似与之前的LinkedHashMap 都可以保证元素遍历顺序和放入的顺序是一致的;
但是LinkedHashMap是非线程安全,而ConcurrentSkipListMap是线程安全的;
数据结构为 跳表
上层白色的链表起到快速定位底层链表的作用;
以插入元素为例:
5. BlockingQueue
经常用来实现生产消费模式,来解耦生产者、消费者线程;
- 队列的选用:
- ArrayBlockingQueue有明确的容量限制,LinkedBlockingQueue取决于在创建时是否指定,SynchronousQueue则干脆不能缓存任何元素;
- ArrayBlockingQueue在空间利用地上比LinkedBlockingQueue紧凑;