HashMap,HashTable和ConcurrentHashMap的基本原理与实现
Posted 每天进步一点点YL
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap,HashTable和ConcurrentHashMap的基本原理与实现相关的知识,希望对你有一定的参考价值。
前言:这几天陆续把后面几篇集合的深入也写完了,先贴上寒假完成的那篇
先来段纯music吧,很好听(感觉听着纯音乐看着代码很享受(๑╹◡╹)ノ"""):
总体图:
HashMap
JDK7之前hashmap又叫散列链表:基于一个数组以及多个链表的实现,hash值冲突的时候,就将对应节点以链表的形式存储。
JDK7的源码解释自行看博客(后面一些问题也考虑到了1.8)
JDK8中,当同一个hash值的链表节点数大于等于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。
下文是基于JDK8的源码解析
HashMap概述
HashMap是由哈希表构成,其实现原理为“拉链法”,横向是数组,纵向是链表组成
存储机制:<key1,value1>传入时,会对key进行hash算法(如hash(key)%len)得到一个值h,则<key1,value1>被存储到下标为h的table数组中,如果再传入<key2,value2>的hash值仍然为h,则将<key2,value2>存到h下标对应的链表中。
HashMap中其存储容器为线性数组。
HashMap重复的key会被覆盖,所以插入时会遍历判断key。
HashMap中允许存在null键,null值。会将其存在table[0]处。
HashMap的实现
HashMap里面实现一个静态内部类Node,其重要的属性有 hash,key , value, next;Node就是HashMap键值对实现的一个基础bean;上面说到HashMap的基础就是一个线性数组,这个数组就是Node[],Map里面的内容都保存在Node[]里面。
(1)私有属性
(2)构造方法
无参构造函数
HashMap中数组的长度始终为2^n 。
(3)Node<K,V>类
(4)元素存储put()
4.1、put(K key, V value): 插入元素,当key存在时覆盖之前的value,返回旧的value值,当key不存在返回null
onlyIfAbsent表示只有在该key对应原来的value为null的时候才插入,也就是说如果value之前存在了,就不会被新put的元素覆盖。
evict参数用于LinkedHashMap中的尾部操作,这里没有实际意义。
(注意:
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 你可能好奇,这里怎么不遍历tree看看有没有key相同的节点呢?其实,putTreeVal内部进行了遍历,存在相同hash时返回被覆盖的TreeNode,否则返回null。
下面的部分代码,使得在链表中搜索时,可以不断的迭代
)
tab = resize()初始化Node<K,V>[]函数及扩容函数
当调用无参构造函数,插入数据时会调用如下代码,初始化一个空间为16的数组
扩容时:当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能
newNode(hash, key, value, null);函数
putTreeValue,treeifyBin自己深入吧
(5)获取元素get()
难理解是put,get(),easy了许多
(6)Fail-Fast机制:
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
(7)总结:
A:HashMap是基于Map接口的实现,存储的是以Node<K,V>为基础的键值对,它可以接收null的键值,是非同步的。
B:HashMap工作原理:
put():插入一个值时,首先通过hash(key)算出对应的下标,并在初始化的Table[]中查找下标所对应的位置,如果当前位置为null,则插入。若不为null,分三种情况:①判断根节点是否为红黑树节点,是,putTreeValue插入(函数自带查重);不是,则遍历链表插入至链表最后,②插入后如果链表长度大于等于8,则转化为红黑树;③插入后如果链表长度小于8,则不变。另在遍历过程中,如果遇到插入key相同的Node节点,则退出遍历,并将新value值覆盖oldvalue。
get():自己看代码,不说了。( ̄︶ ̄)↗
HashTable
HashTable概述
HashTable和HashMap采用相同的存储机制,二者的实现基本一
(HashTable与HashMap的1.7之前的版本相比较),不同的是:
HashTable是线程安全的,内部的方法基本都是synchronized;HashMap是非线程安全的
HashTable不允许有null值的存在(HashTable中调用put方法时,如果key为null,直接抛出NullPointerException)。
HashTable的实现
这里就简单说一下(对比HashMap1.7之前版本的源码)
(1)私有属性
(2)构造函数
HashTable初始化大小为11
(3)Entry<K,V>
(4)元素存储put()
4.1、put(K key, V value): 插入元素,当key存在时覆盖之前的value,返回旧的value值,当key不存在返回null
addEntry图解如下:
(5)获取元素get()
(6)删除元素remove()
图解如下:
ConcurrentHashMap
ConcurrentHashMap代替同步的Map(Collections.synchronized(new HashMap())),众所周知,HashMap是根据散列值分段存储的,同步Map在同步的时候锁住了所有的段,而ConcurrentHashMap加锁的时候根据散列值锁住了散列值锁对应的那段,因此提高了并发性能。
注意:为什么在java1.5中添加了util.concurrent,而不用Collections.synchronized(),因为synchronized锁住的容器,实际上使得并发下对容器的访问变成了串行化访问,这极大的影响了效率,而concurrent锁住的只是部分,容器的其实还是并发访问的
ConcurrentHashMap的内部结构
ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组(直接拿用)
从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment。
这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。
后记:
感谢参考的,提供给我思路的博客文章,大家能看懂吃透是最好的,如果有些地方小伙伴需要再深入的,迷茫的等可以上网进行深入搜索。
以上是关于HashMap,HashTable和ConcurrentHashMap的基本原理与实现的主要内容,如果未能解决你的问题,请参考以下文章