一.容器类接口关系图
(图片来源于网络)
二.HashMap实现分析
2.1HashMap数据结构
是链表散列的数据结构,即数组和链表的结合体
数组:存储区间连续,占用内存严重,寻址容易,插入删除困难
链表:存储区间不连续,占用内存宽松,寻址不容易,插入删除容易
HashMap综合应用了两种数据结构,实现了寻址容易,插入也容易
横着是单链表,竖着是数组
2.2.1JDK1.8之前的并发问题
JDK1.8之前,HashMap的操作,比如addEntry(),resize(),removeEntryForKey()这些操作都没有加锁,导致多线程情况下会出现操作覆盖,操作丢失等问题
2.2.1JDK1.8并发问题
JDK1.8中的HashMap通过成员变量modCount来记录操作的版本
在调用put(),remove(),ensureCapacity()这些会修改数据结构的方法中都会使modCount++
在获取Iterater对象的时候会把modCount赋值给迭代器的expectedModCount变量,此时的expectedModCount和modCount是相等的
迭代的元素的过程中如果调用hasmap自身的方法使集合发生变化,那么modCount肯定会变,此时modCount和expectedModCount肯定不相等,这时会抛出concurrentModificationException,终止迭代操作
由于HashMap是线程不安全的,所以我们需要手动地去使HashMap的线程安全
2.3解决HashMap并发问题的方法
- | 方法 |
---|---|
1 | synchronized关键字 |
2 | Lock同步锁 |
3 | 同步类容器 |
4 | 并发类容器 |
重点讲解同步类容器和并发类容器
三.同步容器
3.1同步容器介绍
在Java中,同步类容器主要包括两类
类型 | 特点 |
---|---|
Vector,Stack,HashTable | 可以独立创建 |
Collections类中提供的静态工厂方法创建的类 | 借助工具类创建,不可以独立创建 |
3.1.1 Vector
实现了List接口,实际上是一个数组,类似于ArrayList,但是vector里的方法都是synchronized方法,即实现了同步措施
3.1.2 Stack
也是一个同步容器,方法也都是synchronized的,实际上继承了Vector类
3.1.3 HashTable
实现了Map接口,和HashMap类似,但是都进行了同步,是线程安全的
3.1.4 Collections
是一个工具提供类(Collection是一个接口,它不是),它提供了大量的方法,比如对集合或容器进行排序,查找等操作,最重要的是它里面提供了几个静态工厂方法来创建同步容器类,有如下的方法:
3.2HashTable
HashTable在JDK1.1就有了,可以看主要方法put(),remove(),get()的源码实现来研究它的同步控制机制
可以看到它的方法都是加了synchronized关键字的,虽然实现了线程安全,但是效率变低了
三.并发容器
同步容器的方法都是加了synchronized关键字的,虽然实现了线程安全,但是效率非常低下,所以在Java5.0以后,开始对多线程进行了重写设计,引入java.util.concurrent包,这些是用来替代原来的非并发容器的
并发容器 | 对应 | 目标 | 原理 |
---|---|---|---|
ConcurrentHashMap | HashMap | 代替HashTable,synchronizedMap,支持复合操作 | JDK6中加入一种更加细粒度的加锁机制,segment"分段锁",JDK8中采用CAS |
CopyOnWriteArrayList | ArrayList | 代替Vector和SynchronizedList | 利用高并发往往是读多写少的特点,对读操作不加锁,对于写操作加锁,对于写操作先复制一份新的集合,在新的集合上面修改,然后把新集合的地址赋值给旧引用,并通过volatile保证其可见性 |
CopyOnWriteSet | HashSet | 代替SynchronizedList | 在CopyOnWriteArrayList的基础上实现,不同的是由于set的无序性,在调用add操作时,调用CopyOnWriteArrayList的addIfAbsent方法,这个方法遍历当前数组,如果该数组已经存在这个值,那就不加了,如果不存在就加入数组的尾部并返回 |
ConCurrentSkipListMap | TreeMap | 代替SynchronizedSortedMap(TreeMap) | 用了跳表的数据结构,SkipList是一种可以代替平衡树的数据结构,默认是按照Key升序的,SkipList让已排序的数据分布在多层链表中,以0-1的随机数选择数据向上攀升与否,通过“空间换时间”的一个算法,ConCurrentSkipListMap提供了一种线程安全的并发访问的排序映射表 |
ConCurrentSkipListSet | TreeSet | 代替SynchronizedSortedSet | 内部基于ConCurrentSkipListMap实现 |
ConCurrentLinkedQueue | 不会阻塞的队列 | 代替Queue | 基于链表实现的FIFO队列(LinkedList的并发版本) |
3.1ConcurrentHashMap数据结构分析
3.1.1JDK1.7之前ConcurrentHashMap的数据结构
基于分段实现的,所有的数据先分段,每个段里面又有小的HashMap
(图片来来源于https://www.jianshu.com/p/39a57484932e)
3.1.2JDK1.8中ConcurrentHashMap的数据结构
不分段,先分数组,数组里面指向链表,如果链表元素个数小于8,那么这个链表是简单的链表,否则这个链表是红黑树的,数组的这个值指向红黑树的根节点
(图片来来源于https://www.jianshu.com/p/39a57484932e)
3.2ConcurrentHashMap同步实现分析
3.2.1JDK1.7之前ConcurrentHashMap的同步实现分析
以put()方法为例
这个put方法没有真正的加锁,只是在一堆计算之后拿到分片,然后再调用分片segment的put方法,而segment是一个可重入锁,segment的put方法里面先加锁再操作之后,最后解锁,通过这个方式来实现线程安全。
3.2.2JDK1.8中ConcurrentHashMap的同步实现分析
我们可以看到cas的操作,这个操作是用来放在数组中的元素的
而Synchronized关键字是用来放链表中的元素
由此可以看到,它在这两个地方的操作都保证了它是线程安全的,而具体后面的链表怎么放,我们可以看到
它会根据链表元素的个数来决定是单链表还是红黑树
在主流程之外,我们可以深入研究三个问题:
1)put如何初始化
initTable()方法,通过CAS操作变量sizeCtl,判断这个变量是否等于零,如果小于零说明没有拿到锁,如果等于零说明拿到锁了,就可以进行初始化了
2)如何扩容
在trypresize里操作,跟上面的类似,也是对变量sizeCtl的CAS的操作
3)如何帮助数据迁移
transfer方法
保证线程安全的操作是CAS操作和synchronized关键字来实现,在迁移之前都做了线程安全的操作