1:HashMap
1.1:基本原理
HashMap是以key,value的存储的容器,它是通过计算key的hash值来进行存储的,在查询的时候也是通过key的hash值进行查询,所有查询效率还是不错的,HashMap不支持key重复,value允许重复,key允许一个为null,底层存储使用到了节点对象数组-单向链表-红黑树存储,红黑数是JDK8加入的主要的是用来优化HashMap的查询,HashMap线程也不是安全的,在高并发容易造成数据混乱
1.2数据结构
-
数组: 数组存储在内存区间空间是连续的,占用空间比较严重,故而空间复杂度很大,我们在往数组里面插入元素or删除元素都需要在内存中移动元素的位置,来进行修改(复制,移动),这样操作元素就很耗时间,数组来是线性表,线性表的一个特性就是元素都是以一根线的形式在内存中存储,我们查询只需要根据元素索引的位置进行查询就行了,查询时间复杂度为O(1),数组特点容易查询,插入删除慢
-
链表:链表在内存中存储数据离散,占用空间比较宽松,往链表结构存储数据,会在内存中生产两块空间,一块存储数据,一块记录数据存储的nxet(指针),所以新增or删除只需更改元素上的指针,查询的话就需要一个一个元素往下找下去,数据复杂度为O(n),故而查询慢,新增和删
-
hash表: 存储通过计算键值对key的hash值来进行存储的,key就像数组的索引,所以查询非常块,时间复杂度为O(1),hash占用内存也是比较少的
1.3:java7之前实现
HashMap在java7是以数组加链表实现的,数组里面是一个单向链表,链表每一个元素都是Entry对象,Entry对象有4个字段,key,value,hash值,链表的next指针
1.4:java8实现
HashMap在Java8添加了红黑树存储,存储变成数组-单向链表-红黑树
HashMap在java7的时候,查询需要根据计算key的hash值来定位数组的具体下标,但是之后,需要顺着链表一个一个比较才能找到我们需要的数据,所以在链表这一步时间复杂度变成O(n),n主要取决于链表的长度,为了降低这部分开销,在Java8中当链表元素超过8个之后,会将链表转换成红黑树存储,为这些位置元素查询时间降低为O(logN)
put方法源码
// 参数onlyIfAbsent表示是否替换原值
// 参数evict我们可以忽略它,它主要用来区别通过put添加还是创建时初始化数据的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 空表,需要初始化
if ((tab = table) == null || (n = tab.length) == 0)
// resize()不仅用来调整大小,还用来进行初始化配置
n = (tab = resize()).length;
// (n - 1) & hash这种方式也熟悉了吧?都在分析ArrayDeque中有体现
//这里就是看下在hash位置有没有元素,实际位置是hash % (length-1)
if ((p = tab[i = (n - 1) & hash]) == null)
// 将元素直接插进去
tab[i] = newNode(hash, key, value, null);
else {
//这时就需要链表或红黑树了
// e是用来查看是不是待插入的元素已经有了,有就替换
Node<K,V> e; K k;
// p是存储在当前位置的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //要插入的元素就是p,这说明目的是修改值
// p是一个树节点
else if (p instanceof TreeNode)
// 把节点添加到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 这时候就是链表结构了,要把待插入元素挂在链尾
for (int binCount = 0; ; ++binCount) {
//向后循环
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表比较长,需要树化,
// 由于初始即为p.next,所以当插入第8个元素才会树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 找到了对应元素,就可以停止了
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 继续向后
p = e;
}
}
// e就是被替换出来的元素,这时候就是修改元素值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 默认为空实现,允许我们修改完成后做一些操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size太大,达到了capacity的0.75,需要扩容
if (++size > threshold)
resize();
// 默认也是空实现,允许我们插入完成后做一些操作
afterNodeInsertion(evict);
return null;
}
1.5:扩容机制
- capacity: 当前数组容量,始终保持2^n(2的n次幂次方)
,可以扩容,扩容后数组大小为当前的2倍 - loadFactor: 负载因子,默认0.75
- threshold: 扩容阈值,等于capacity*loadFactor
2:ConcurrentHashMap
1:基本原理
ConcurrentHashMap基本原理和HashMap差不多,存储也是通过数组,单向链表,红黑树存储,但是相较于HashMap线程不安全,ConcurrentHashMap是线程安全的,且支持并发,线程安全主要是用通过synchronized-CAS来实现
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
1.1:java7实现
ConcurrentHashMap存储是通过Segment数组加链表实现的,Segment对象也能称之为一段或者槽,ConcurrentHashMap之所以线程安全,是Segment继承了ReentrantLock来加锁,每次锁住一个Segment对象,这样就保证了并发发下全局的线程安全
并行度:concurrencyLevel 并发数默认16个,也就是说ConcurrentHashMap有16个Segments,所以理论上最多同时支持16个线程并发写,前提他们的操作分别分布在不同的segemt上,默认并发数在初始化后不能扩容,但是在还未初始化的时候设置其他值,
1.2:java8实现
java8对ConcurrentHashMap有较大改动,其中最大的是把Segment数组移除了,换成了node数组存储,保证线安全程换成了JVM级别的synchronized关键字和CAS算法,其次也加入了红黑树,变成node数组+单向链表+红黑树存储,非常类似HashMap