概述
文章目录
- HashMap
- HashMap使用红黑树而不使用AVL
- 扩容机制
- Entry类
- 往HashMap中添加元素——put(key,value)函数
- 从HashMap取元素——get(Object key)函数
- keySet()和entrySet()
- Hashtable
- ConcurrentHashMap
- 添加元素——put方法
- 取出元素——get方法
- JDK1.7和JDK1.8比较
- HashMap和Hashtable的区别
HashMap
首先有一个table数组,数组的每个节点都是Entry类型。实际上就是数组+链表的形式。
- HashMap初始化过程就是新建一个大小为capacity,类型为Entry的数组。默认生成一个大小为16,填装因子为0.75的HashMap。(填装因子越小,备用内存越多,冲突越小)
- 如果链表长度大于8,而table长度小于MIN_TREEIFY_CAPACITY(默认值是64)时,还不会变成红黑树,而是调用resize()方法扩容。
- 如果链表长度大于8,且table大于等于64了,就将链表转换为红黑树。
HashMap实现原理及扩容机制
深入解析hashmap
HashMap使用红黑树而不使用AVL
- 红黑树适合修改操作,AVL更适合查找操作
- AVL树的旋转比红黑树的旋转更加难以平衡和调试
hashmap使用红黑树而不使用AVL
扩容机制
关于为什么初始容量是2的n次幂扩容是2倍
JDK1.7:把原table的Node用头插法放到心得table中(新table和旧table中顺序是反的)
indexFor():计算在新table中的下标用。hash值和(length-1)按位与
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
JDK1.8:hash值与旧table的length相与,如果为0,就说明在新table中下标和旧table中相同,如果相与为1,就说明新table下标为原下标加原table长度。
在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。
Entry类
Entry类是HashMap的内部类,包含(next——指向一个Entry的指针,key,value,hash)
往HashMap中添加元素——put(key,value)函数
- 如果key为null,先在table[0]链上查找是否有key为null的Entry对象,如果有则覆盖,没有则新生成一个key为null的Entry对象,然后放在table[0]的位置
- 如果key不为null,则通过hash(key)算出hash值以此确定放在table的哪条链。
- 如果该链上已经有一个key也相同(由2可知hash相同)的,则用新的value覆盖之前的值。
- 如果没有key相同的,则新生成一个Entry对象插入到链表中
重点:
- HashMap中的key可以为null,此时hash=0
- 用户插入(key,value)时不是直接放到HashMap中,而是新生成一个Entry对象后再插入到HashMap中(可能涉及扩充)。
- 如果对应链上(hash相同)有key相同的Entry,则直接覆盖value,不生成Entry对象。
从HashMap取元素——get(Object key)函数
- 如果key为null,则在table[0]这条链上找,找到返回value值,否则返回null
- 通过getEntry(key)——先通过hash(key)找到hash值确定在table的位置,然后在该链查找key相同的,没有找到返回null,找到了返回该entry。
- 通过entry.getValue()返回2中entry的vaule值
重点
- 先由hash值找到链,再用key在链上找
- key为null则只在table[0]上找
- getEntry()返回的是Entry对象,而get()需要的是Entry对象的value值
- 不能由get方法判断HashMap中是否存在某个键,因为get方法返回null时,既可以表示HashMap中没有该键,也可以表示该键所对应的值为null。应该用containsKey()来判断。
keySet()和entrySet()
- 都是用来遍历HashMap的
- keySet的迭代器可以依次获取下一个key值,再根据key值取得value值
- entrySet可以通过迭代器直接获得下一个entry值,所以entrySet比keySet快
Hashtable
- HashTable是线程安全的
- HashTable线程安全的策略实现代价非常大
Hashtable是线程安全的哈希表,通过synchronized来保证线程安全,get/put所有相关操作都是synchronized的。多线程通过同一个“对象的同步锁”来实现并发控制,相当于给整个哈希表加了一大把锁,当一个线程访问Hashtable的同步方法时,其他线程如果也访问Hashtable的同步方法可能会进入阻塞状态。
ConcurrentHashMap
线程安全的哈希表
- JDK1.7中,通过“分段锁”来保证线程安全,容器中有多把锁,每一把锁锁一段数据。本质上也是一个“可重入的互斥锁”(ReentrantLock),多线程对同一个片段访问,互斥;对不同片段访问,可以同步进行。ReentrantLock+segment+HashEntry
- JDK1.8中,使用CAS原子更新,volatile关键字,synchronized可重入锁实现。
synchronized+CAS+HashEntry+红黑树
(JDK1.8中) 主要采用数组+链表+红黑树的形式实现。
Node:ConcurrentHashMap存储结构的基本单元,继承于HashMap的Entry,用于存储数据,对value和next设置了volatile同步锁
TreeNode:继承于Node,数据结构换成了二叉树结构,是红黑树的数据存储结构。通过TreeNode代替Node来转换成红黑树
TreeBin:存储树形结构的容器,是封装TreeNode的容器,提供转换为红黑树的一些条件和锁的控制
ConcurrentHashMap实现原理及源码分析
添加元素——put方法
- 数组是否初始化,没有则调用initTable()方法进行初始化过程(sizeCtl=0,其他线程正在初始化;sizeCtl=-1,正在初始化,防止其他线程进入;初始化完成,sizeCtl改为0.75*n)
- 通过计算hash值来判断放到数组哪个位置
- 没有hash冲突则直接CAS自旋锁插入
- 如果有hash冲突,则取出数组中的该节点。如果该hash值为MOVED(-1),表示正在对该数组进行扩容,那么协助扩容
- 判断取出的节点位置存放的是链表还是树
- 链表则遍历整个链表,若有key相等,说明是同一个key,则直接覆盖之前的value,否则添加到链表的末尾
- 如果是树,则调用putTreeVal把元素添加到树中
- 添加完成后调用addCount()方法统计size,判断目前该节点有多少个节点,如果达到8个以上,则用treeifyBin()方法将该处的链表转为树,或者扩容数组
扩容transfer
- 构建一个nextTable,容量是原来的两倍,由单线程完成。
- 将原来的元素复制到nextTable中,允许多线程操作
- 扩容的时候,可以对数组进行读写操作
取出元素——get方法
计算hash值判断在数组哪个位置,然后遍历链表或树。
JDK1.7和JDK1.8比较
- JDK1.8的数据结构更加坚定,去掉了Segment,使用synchronized进行同步锁粒度降低,不需要分段锁概念,实现复杂度增加
- JDK1.8用红黑树优化链表
- 用内置锁synchronized代替重入锁ReentrantLock
HashMap和Hashtable的区别
- HashMap是Hashtable的轻量级实现,HashMap允许null键值,而Hashtable不允许
- HashMap没有contains方法,而是containsKey和containsValue
- Hashtable是线程安全的,HashMap不是线程安全的
- HashMap使用Iterator,Hashtable使用Enumeration
- Hashtable中,hash数组默认大小是11,增加的方式是old*2+1,HashMap中hash数组的默认大小是16,而且一定是2的倍数
最后
以上就是懦弱小馒头为你收集整理的JAVA中HashMap、HashTable和ConcurrentHashMapHashMapHashtableConcurrentHashMapHashMap和Hashtable的区别的全部内容,希望文章能够帮你解决JAVA中HashMap、HashTable和ConcurrentHashMapHashMapHashtableConcurrentHashMapHashMap和Hashtable的区别所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复