1. HashMap
-
put 的时候是插链表。
-
ReHash 的时候也是头插链表。
在jdk1.8之前是插入头部的,在jdk1.8中是插入尾部的。 -
多线程 put 链表会成环,获取环链表上的某个k时会出现死循环。
-
多线程put 可能导致某个值被覆盖或丢失。
-
初始容量为16,负载因子为 0.75,超过负载扩容一倍
为什么初始是16:
int index =key.hashCode()&(length-1);
1 减少hash碰撞
2 提高map查询效率
3 分配过小防止频繁扩容
4 分配过大浪费资源 -
为什么链表的长度为8是变成红黑树?为什么为6时又变成链表?
1)理想情况下随机hashCode算法下所有bin中节点的分布频率会遵循泊松分布,我们可以看到,一个bin中链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。
2)红黑树的平均查找长度是log(n);链表长度查询的平均长度为 n/2
log(8)=3 8/2=4 为8时才多查找一次
log(6)=2.6 6/2=3
2. ConcurrentHashMap
2.1. JDK1.8:
中ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
只要不发生hash不冲突,就不会出现并发获得锁的情况。它首先使用无锁操作CAS插入头结点,如果插入失败,说明已经有别的线程插入头结点了,再次循环进行操作。如果头结点已经存在,则通过synchronized获得头结点锁,进行后续的操作。性能比segment分段锁又再次提升。
put操作:
- 判断Node[]数组是否初始化,没有则进行初始化操作
- 通过hash定位Node[]数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环。
- 检查到内部正在扩容,如果正在扩容,就帮助它一块扩容。
- 如果f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
4.1 如果是Node(链表结构)则执行链表的添加操作。
4.2 如果是TreeNode(树型结果)则执行树添加操作。 - 判断链表长度已经达到临界值8 就需要把链表转换为树结构。
CAS
如果从内存地址V取值,原来值为A,修改为B,之后又将值修改为A,CAS会认为该值从来没变过。
解决思路:加版本号。
2.2. JDK 1.7:
1.一个CocurrentHashMap(CHM)包含一个Segment数组;一个Segment元素里包含一个HashEntry数组;每个HashEntry是一个链表结构的元素。
2. HashEntry则用于存储键值对数据;
3. Segment 继承可重入锁(ReentrantLock),扮演锁的角色;。
2.3. 为什么get方法不需要加锁?
get方法将要使用的共享变量都定义成了volatile类型,
transient volatile int count;
volatile V value;
定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值。get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。
Java内存模型的happen before原则,对volatile字段的写操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的
2.3.1 volatile
volatile 的特性
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(可见性)
- 禁止进行指令重排序。(实现有序性)
- volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
volatile 的实现原理:
1)volatile 可见性实现
- volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现。
内存屏障,是一个 CPU 指令。
JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序
2)volatile 有序性实现
- happens-before 规则中有一条是 :对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
- volatile 禁止重排序
(1)
(2)