提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- Arrary ArraryList和LinkedList的区别
- ArrayList的扩容机制
- HashMap的put方法的执行过程
- 什么是哈希表
- HashMap与HashTable(哈希表/散列表)区别
- morris遍历具体
- 层次遍历
- 栈与队列的区别
- HashMap 和 HashTable 的区别
- HashMap 和 HashSet 的区别
- HashMap 的底层结构
- HashMap 的长度为什么是 2 的幂次方
- HashMap 多线程操作导致死循环问题
- HashMap 线程安全的实现有哪些
- 讲一下 HashMap put 的过程
- ConcurrentHashMap 底层实现
- Integer 缓存池
- Arrays.asList 获得的 List 应该注意什么
- Collection 和 Collections 的区别
- 你知道 fail-fast 和 fail-safe 吗
- ArrayList、LinkedList 和 Vector 的区别
- HashMap 的数据结构?
- HashMap 的工作原理?(特点是扩容、key不重复、桶结构(链表+红黑树)、value可以重复)
- 当两个对象的 hashCode 相同会发生什么?
- 你知道 hash 的实现吗?为什么要这样实现?
- 为什么要用异或运算符?
- HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?
- HashMap中put方法的过程?
- 数组扩容的过程?
- 拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
- 说说你对红黑树的见解?
- jdk8中对HashMap做了哪些改变?
- HashMap,LinkedHashMap,TreeMap 有什么区别?
- HashMap & TreeMap & LinkedHashMap 使用场景?
- HashMap 和 HashTable 有什么区别?
- Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?
- HashMap & ConcurrentHashMap 的区别?
- 为什么 ConcurrentHashMap 比 HashTable 效率要高?
- 针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)
- ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
- ConcurrentHashMap 简单介绍?
- ConcurrentHashMap 的并发度是什么?
- 总结
前言
没什么用的前言
Arrary ArraryList和LinkedList的区别
ArraryList | LinkedList |
---|---|
实现了RandomAccess接口,支持快速随机访问 | 只能从链表头开始访问,查询慢 |
添加某个元素,添加位置后面的元素索引都要+1 | 添加某个元素,只需要改变位置前后的索引 |
删除某个元素,删除位置后面的元素索引都要-1 | 删除某个元素,只需要改变位置前后的索引 |
总结:ArraryList查询快,LinkedList增删快.
Arrary指数组可以容纳基本类型数据,不能扩容,没有方法
ArrayList的扩容机制
新容量是旧容量的1.5倍
HashMap的put方法的执行过程
什么是哈希表
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数(哈希函数),存放记录的数组称做散列表。
哈希表其实是一种数据结构
哈希表本质上是个数组,底层实现是在数组上然后在加工, 称 哈希表。
实现哈希表的方式分为:
数组+链表
数组+[二叉树]
HashMap与HashTable(哈希表/散列表)区别
1、继承的父类不同
HashMap继承自AbstractMap类。但二者都实现了Map接口。
Hashtable继承自Dictionary类,Dictionary类是一个已经被废弃的类(见其源码中的注释)。父类都被废弃,自然而然也没人用它的子类Hashtable了。
2、HashMap线程不安全,HashTable线程安全
3.包含的contains方法不同
HashMap是没有contains方法的,而包括containsValue和containsKey方法;hashtable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。
## 二叉树遍历 4种
四种主要的遍历思想为:
前序遍历:根结点 —> 左子树 —> 右子树
中序遍历:左子树—> 根结点 —> 右子树
后序遍历:左子树 —> 右子树 —> 根结点
层次遍历:只需按层次遍历即可
morris遍历具体
Morris一种遍历二叉树的方式,并且时间复杂度为O(N),额外空间复杂度O(1)。 Morris遍历通过利用树中叶子节点大量空闲指针的方式,以达到节省空间的目的。 在Morris遍历的基础上可以加工出先序遍历、中序遍历和后续遍历。
层次遍历
层次遍历的代码比较简单,只需要一个队列即可,先在队列中加入根结点。之后对于任意一个结点来说,在其出队列的时候,访问之。同时如果左孩子和右孩子有不为空的,入队列
栈与队列的区别
一、队列与栈的定义:
队列(Queue):是限定能在表的一端进行插入和在另一端进行删除操作的线性表;(一头插一头删)
栈(Stack):是限定只能在表的一端进行插入和删除操作的线性表。(插和删都在一端)
二、队列与栈的区
队列和栈是两种不同的数据结构。它们有以下区别:
(1)操作的名称不同。
队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。
(2)操作的限定不同。
队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
(3)操作的规则不同。
队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。
(4)遍历数据速度不同。
队列是基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间,因为在遍历的过程中不影响数据结构,所以遍历速度要快。栈是只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,而且在遍历数据的同时需要为数据开辟临时空间,保持数据在遍历前的一致性。
HashMap 和 HashTable 的区别
相同点
HashMap 和 HashTable 都是基于哈希表实现的,其内部每个元素都是 key-value 键值对,HashMap 和 HashTable 都实现了 Map、Cloneable、Serializable 接口。
不同点
- 父类不同:HashMap 继承了 AbstractMap 类,而 HashTable 继承了 Dictionary 类,也叫字典类
- 空值不同:HashMap 允许空的 key 和 value 值,HashTable 不允许空的 key 和 value 值。HashMap 会把 Null key 当做普通的 key 对待。不允许 null key 重复。
- 线程安全性:HashMap 不是线程安全的,如果多个外部操作同时修改 HashMap 的数据结构比如 add 或者是 delete,必须进行同步操作,仅仅对 key 或者 value 的修改不是改变数据结构的操作。可以选择构造线程安全的 Map 比如 Collections.synchronizedMap 或者是 ConcurrentHashMap(同步的hashmap)。而 HashTable 本身就是线程安全的容器。性能方面:虽然 HashMap 和 HashTable 都是基于单链表的,但是 HashMap 进行 put 或者 get 操作,可以达到常数时间的性能;而 HashTable 的 put 和 get 操作都是加了 synchronized 锁的,所以效率很差。(安全的hashtable效率差)
- 初始容量不同:HashTable 的初始长度是11,之后每次扩充容量变为之前的 2n+1(hashtable扩容前后都一定是奇数容量)而 HashMap 的初始长度为16,之后每次扩充变为原来的两倍。创建时,如果给定了容量初始值,那么HashTable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。
HashMap 和 HashSet 的区别
HashSet 继承于 AbstractSet 接口,实现了 Set、Cloneable,、java.io.Serializable 接口。HashSet 不允许集合中出现重复的值。HashSet 底层其实就是 HashMap,所有对 HashSet 的操作其实就是对 HashMap 的操作。所以 HashSet 也不保证集合的顺序,也不是线程安全的容器。
HashMap 的底层结构
JDK1.7 中,HashMap 采用位桶 + 链表的实现,即使用链表来处理冲突,同一 hash 值的链表都存储在一个数组中。但是当位于一个桶中的元素较多,即 hash 值相等的元素较多时,通过 key 值依次查找的效率较低。
所以,与 JDK 1.7 相比,JDK 1.8 在底层结构方面做了一些改变,当每个桶中元素大于 8 的时候,会转变为红黑树,目的就是优化查询效率。
HashMap 的长度为什么是 2 的幂次方
这道题我想了几天,之前和群里小伙伴们探讨每日一题的时候,问他们为什么 length%hash == (n - 1) & hash,它们说相等的前提是 length 的长度 2 的幂次方,然后我回了一句难道 length 还能不是 2 的幂次方吗?其实是我没有搞懂因果关系,因为 HashMap 的长度是 2 的幂次方,所以使用余数来判断在桶中的下标。如果 length 的长度不是 2 的幂次方,小伙伴们可以举个例子来试试
例如长度为 9 时候,3 & (9-1) = 0,2 & (9-1) = 0 ,都在 0 上,碰撞了;
这样会增大 HashMap 碰撞的几率。
HashMap 多线程操作导致死循环问题
HashMap 不是一个线程安全的容器,在高并发场景下,应该使用 ConcurrentHashMap,在多线程场景下使用 HashMap 会造成死循环问题(基于 JDK1.7),出现问题的位置在 rehash 处,也就是
do {
Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
这是 JDK1.7 的 rehash 代码片段,在并发的场景下会形成环。
JDK1.8 也会造成死循环问题。
HashMap 线程安全的实现有哪些
因为 HashMap 不是一个线程安全的容器,所以并发场景下推荐使用 ConcurrentHashMap,或者使用线程安全的 HashMap,使用 Collections 包下的线程安全的容器,比如说
Collections.synchronizedMap(new HashMap());
还可以使用 HashTable ,它也是线程安全的容器,基于 key-value 存储,经常用 HashMap 和 HashTable 做比较就是因为 HashTable 的数据结构和 HashMap 相同。
上面效率最高的就是 ConcurrentHashMap。
讲一下 HashMap put 的过程
首先会使用 hash 函数来计算 key,然后执行真正的插入方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果table 为null 或者没有为table分配内存,就resize一次if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 指定hash值节点为空则直接插入,这个(n - 1) & hash才是表中真正的哈希if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 如果不为空else {
Node<K,V> e; K k;
// 计算表中的这个真正的哈希值与要插入的key.hash相比if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 若不同的话,并且当前节点已经在 TreeNode 上了else if (p instanceof TreeNode)
// 采用红黑树存储方式
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// key.hash 不同并且也不再 TreeNode 上,在链表上找到 p.next==nullelse {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 在表尾插入
p.next = newNode(hash, key, value, null);
// 新增节点后如果节点个数到达阈值,则进入 treeifyBin() 进行再次判断if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果找到了同hash、key的节点,那么直接退出循环if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 更新 p 指向下一节点
p = e;
}
}
// map中含有旧值,返回旧值if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// map调整次数 + 1
++modCount;
// 键值对的数量达到阈值,需要扩容if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashMap put 方法的核心就是在 putval 方法,它的插入过程如下
-
首先会判断 HashMap 中是否是新构建的,
-
如果是的话会首先进行 resize然后判断需要插入的元素在 HashMap 中是否已经存在(说明出现了碰撞情况),如果不存在,直接生成新的k-v 节点存放,
-
再判断是否需要扩容。如果要插入的元素已经存在的话,说明发生了冲突,这就会转换成链表或者红黑树来解决冲突,
-
首先判断链表中的 hash,key 是否相等,如果相等的话,就用新值替换旧值,如果节点是属于 TreeNode 类型,会直接在红黑树中进行处理,如果 hash ,key 不相等也不属于 TreeNode 类型,会直接转换为链表处理,进行链表遍历,如果链表的 next 节点是 null,判断是否转换为红黑树,如果不转换的话,在遍历过程中找到 key 完全相等的节点,则用新节点替换老节点
ConcurrentHashMap 底层实现
ConcurrentHashMap 是线程安全的 Map,它也是高并发场景下的首选数据结构,ConcurrentHashMap 底层是使用分段锁来实现的。
Integer 缓存池
Integer 缓存池也就是 IntegerCache ,它是 Integer 的静态内部类。
它的默认值用于缓存 -128 - 127 之间的数字,如果有 -128 - 127 之间的数字的话,使用 new Integer 不用创建对象,会直接从缓存池中取,此操作会减少堆中对象的分配,有利于提高程序的运行效率。(和string的情况相似)
例如创建一个 Integer a = 24,其实是调用 Integer 的 valueOf ,可以通过反编译得出这个结论
然后我们看一下 valueOf 方法
如果在指定缓存池范围内的话,会直接返回缓存的值而不用创建新的 Integer 对象。
缓存的大小可以使用 XX:AutoBoxCacheMax 来指定,在 VM 初始化时,java.lang.Integer.IntegerCache.high 属性会设置和保存在 sun.misc.VM 的私有系统属性中。
Arrays.asList 获得的 List 应该注意什么
Arrays.asList 是 Array 中的一个静态方法,它能够实现把数组转换成为 List 序列,需要注意下面几点
- Arrays.asList 转换完成后的 List 不能再进行结构化的修改,什么是结构化的修改?就是不能再进行任何 List 元素的增加或者减少的操作。
public static void main(String[] args) {
Integer[] integer = new Integer[] { 1, 2, 3, 4 };
List integetList = Arrays.asList(integer);
integetList.add(5);
}
结果会直接抛出
Exception in thread "main" java.lang.UnsupportedOperationException
我们看一下源码就能发现问题
// 这是 java.util.Arrays 的内部类,而不是 java.util.ArrayList
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
继承 AbstractList 中对 add、remove、set 方法是直接抛异常的,也就是说如果继承的子类没有去重写这些方法,那么子类的实例去调用这些方法是会直接抛异常的。
下面是AbstractList中方法的定义,我们可以看到具体抛出的异常:
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
虽然 set 方法也抛出了一场,但是由于 内部类 ArrayList 重写了 set 方法,所以支持其可以对元素进行修改。
- Arrays.asList 不支持基础类型的转换
Java 中的基础数据类型(byte,short,int,long,float,double,boolean)是不支持使用 Arrays.asList 方法去转换的
Collection 和 Collections 的区别
Collection 和 Collections 都是位于 java.util 包下的类
Collection 是集合类的父类,它是一个顶级接口,大部分抽象类比如说 AbstractList、AbstractSet 都继承了 Collection 类,Collection 类只定义一节标准方法比如说 add、remove、set、equals 等,具体的方法由抽象类或者实现类去实现。
Collections 是集合类的工具类,Collections 提供了一些工具类的基本使用
- sort 方法,对当前集合进行排序, 实现 Comparable 接口的类,只能使用一种排序方案,这种方案叫做自然比较比如实现线程安全的容器 Collections.synchronizedList、 Collections.synchronizedMap 等reverse 反转,使用 reverse 方法可以根据元素的自然顺序 对指定列表按降序进行排序。fill,使用指定元素替换指定列表中的所有元素。
有很多用法,读者可以翻阅 Collections 的源码查看,Collections 不能进行实例化,所以 Collections 中的方法都是由 Collections.方法 直接调用。
你知道 fail-fast 和 fail-safe 吗
fail-fast 是 Java 中的一种快速失败机制,java.util 包下所有的集合都是快速失败的,快速失败会抛出 ConcurrentModificationException 异常,fail-fast 你可以把它理解为一种快速检测机制,它只能用来检测错误,不会对错误进行恢复,fail-fast 不一定只在多线程环境下存在,ArrayList 也会抛出这个异常,主要原因是由于 modCount 不等于 expectedModCount。
fail-safe 是 Java 中的一种 安全失败 机制,它表示的是在遍历时不是直接在原集合上进行访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException。java.util.concurrent 包下的容器都是安全失败的,可以在多线程条件下使用,并发修改。
ArrayList、LinkedList 和 Vector 的区别
这也是一道老生常谈的问题了
ArrayList、LinkedList、Vector 都是位于 java.util 包下的工具类,它们都实现了 List 接口。
-
ArrayList 的底层是动态数组,它是基于数组的特性而演变出来的,所以ArrayList 遍历访问非常快,但是增删比较慢,因为会涉及到数组的拷贝。ArrayList 是一个非线程安全的容器,在并发场景下会造成问题,如果想使用线程安全的容器的话,推荐使用 Collections.synchronizedList;
-
ArrayList 在扩容时会增加 50% 的容量。LinkedList 的底层是双向链表,所以 LinkedList 的增加和删除非常快,只需把元素删除,把各自的指针指向新的元素即可。但是 LinkedList 遍历比较慢,因为只有每次访问一个元素才能知道下一个元素的值。LinkedList 也是一个非线程安全的容器,推荐使用 Collections.synchronizedList
-
Vector 向量是最早出现的集合容器,Vector 是一个线程安全的容器,它的每个方法都粗暴的加上了 synchronized 锁,所以它的增删、遍历效率都很低。Vector 在扩容时,它的容量会增加一倍。
HashMap 的数据结构?
A:哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。
transient Node<K,V>\[\] table;
HashMap 的工作原理?(特点是扩容、key不重复、桶结构(链表+红黑树)、value可以重复)
HashMap 底层是 hash 数组和单向链表实现,数组中的每个元素都是链表,由 Node 内部类(实现 Map.Entry接口)实现,HashMap 通过 put & get 方法存储和获取。
存储对象时,将 K/V 键值传给 put() 方法:
①、调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;(set是按照hash排序的)
②、调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容resize 为 2n);
③、i.如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞;
ii.如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对;
iii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。
(JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法)(注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树)
获取对象时,将 K 传给 get() 方法:①、调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;②、顺序遍历链表,equals()方法查找相同 Node 链表中 K 值对应的 V 值。
hashCode 是定位的,存储位置;equals是定性的,比较两者是否相等。
当两个对象的 hashCode 相同会发生什么?
因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。
你知道 hash 的实现吗?为什么要这样实现?
JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
为什么要用异或运算符?
保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变**。尽可能的减少碰撞**。
HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?
①、table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;
②、loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75,比如table 数组大小为 16,装载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时,table就需要动态扩容;
③、扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold)
④、如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。
HashMap中put方法的过程?
答:“调用哈希函数获取Key对应的hash值,再计算其数组下标;
如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;
如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表 ;
如果结点的key已经存在,则替换其value即可;
如果集合中的键值对大于12,调用resize方法进行数组扩容。”
数组扩容的过程?
创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。
拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?
之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。推荐:面试问红黑树,我脸都绿了。
而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于8的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。
说说你对红黑树的见解?
- 每个节点非红即黑根节点总是黑色的如果节点是红色的,则它的子节点必须是黑色的(反之不一定)每个叶子节点都是黑色的空节点(NIL节点)从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
jdk8中对HashMap做了哪些改变?
在java 1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)
发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入
在java 1.8中,Entry被Node替代(换了一个马甲。
HashMap,LinkedHashMap,TreeMap 有什么区别?
LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;
TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)
HashMap & TreeMap & LinkedHashMap 使用场景?
一般情况下,使用最多的是 HashMap。
HashMap:在 Map 中插入、删除和定位元素时;
TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。
HashMap 和 HashTable 有什么区别?
①、HashMap 是线程不安全的,HashTable 是线程安全的;
②、由于线程安全,所以 HashTable 的效率比不上 HashMap;
③、HashMap最多只允许一条记录的键为null,允许多条记录的值为null,而 HashTable不允许;
④、HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;
⑤、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode
Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?
ConcurrentHashMap 类(是 Java并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。
HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
而针对 ConcurrentHashMap,在 JDK 1.7 中采用 分段锁的方式;JDK 1.8 中直接采用了CAS(无锁算法)+ synchronized。
HashMap & ConcurrentHashMap 的区别?
除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有null,但是ConCurrentHashMap 都不允许。
为什么 ConcurrentHashMap 比 HashTable 效率要高?
HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;
ConcurrentHashMap
- JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。
针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)
JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;
②、HashEntry 用来封装映射表的键-值对;
③、每个桶是由若干个 HashEntry 对象链接起来的链表
JDK 1.8 中,采用Node + CAS + Synchronized来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。
ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
①、粒度降低了;
②、JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
③、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。
ConcurrentHashMap 简单介绍?
①、重要的常量:
private transient volatile int sizeCtl;
当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;
当为 0 时,表示 table 还没有初始化;
当为其他正数时,表示初始化或者下一次进行扩容的大小。
②、数据结构:
Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;
TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;
TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。
③、存储对象时(put() 方法):
如果没有初始化,就调用 initTable() 方法来进行初始化;
如果没有 hash 冲突就直接 CAS 无锁插入;
如果需要扩容,就先进行扩容;
如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环
如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。
④、扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍。
helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
⑤、获取对象时(get()方法):
计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;
如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找该结点,匹配就返回;
以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。
ConcurrentHashMap 的并发度是什么?
程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。
当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)
总结
没什么好总结的