参考和转自https://blog.csdn.net/qq_35190492/article/details/103589011
HashMap:
问:HashMap初始容量为16,虽然16是2的幂,但8和32也是。为何偏偏选择16作为初始容量?
答:个人感觉其实就是一个经验值,定义16没有很特殊的原因,只要是2的次幂,其实用8、32都差不多,无非用16作者认为这个初始容量更能符合常用而已。
问:HashMap中的链表大小超过8个时会自动转化为红黑树,当删除小于6时重新变为链表,为什么?
答:根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
问:HashMap在多线程环境下存在线程安全问题,那你一般都是怎么处理这种情况的?
答:一般在多线程场景,会使用好几种不同方式去代替:
- 使用Collections.synchronizedMap(Map)创建线程安全的map集合。
- Hashtable。
- ConcurrentHashMap。
不过由于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,他的性能和效率明显高于前两者。
问:Collections.synchronizedMap是怎么实现线程安全的你有了解过么?
答:在synchronizedMap方法内部new了一个SynchronizedMap内部类并传入一个Map类型的构造参数,还有互斥锁mutex。如下图
Collections.synchronizedMap(new HashMap<>(16));在调用此方法时需要传入一个Map,可以看到有两个构造器,若你传入了mutex参数,则将对象互斥锁赋值为传入的对象。若没有,则将对象互斥锁赋值为this,即调用synchronizedMap的对象,就是上面的Map。创建出synchronizedMap之后,再操作map的时候,就会对方法上锁。如下图
问:聊一下HashTable以及与HashMap的不同?
答:跟HashMap相比HashTable是线程安全的,key和value不允许为null(因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理),适合在多线程的情况下使用,但是效率可不太乐观。看源码发现它内部对数据操作的时候都会上锁,所以效率比较低下。
Key-Value:HashTable的key和value不允许为null,HashMap的key和value都可以为 null。
实现方式不同:HashTable 继承了 Dictionary类,而 HashMap 继承的是 AbstractMap 类。
线程安全:HashMap线程不安全, HashTable线程安全。
初始化容量不同:HashMap 的初始容量为:16,Hashtable 初始容量为:11,两者的负载因子默认都是:0.75。
扩容机制不同:当现有容量大于总容量 * 负载因子时,HashMap 扩容规则为当前容量翻倍,Hashtable 扩容规则为当前容量翻倍 + 1。
迭代器不同:HashMap 中的 Iterator 迭代器是 fail-fast 的,而 Hashtable 的 Enumerator 不是 fail-fast 的。所以,当其他线程改变了HashMap 的结构,如:增加、删除元素,将会抛出ConcurrentModificationException 异常,而 Hashtable 则不会。
问:fail-fast是啥?
答:快速失败(fail-fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增、删、改),则会抛出ConcurrentModificationException。
其原理是:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)算是一种安全机制吧。
Tip:安全失败(fail—safe)大家也可以了解下,java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
问:这样的场景,我们在开发过程中都是使用ConcurrentHashMap,他的并发的相比前两者好很多。那你跟我说说他的数据结构吧,以及为啥他并发度这么高?
答:ConcurrentHashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。
在jdk1.7中的数据结构:是由一个Segment数组和多个HashEntry组成,主要实现原理是实现了锁分离的思路解决了多线程的安全问题。和 HashMap 一样,仍然是数组加链表。
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
// 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
transient volatile HashEntry<K,V>[] table;
transient int count;
// 记得快速失败(fail—fast)么?
transient int modCount;
// 大小
transient int threshold;
// 负载因子
final float loadFactor;
}
HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。
问:volatile的特性是啥?
答:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的(实现可见性)。禁止进行指令重排序(实现有序性)。
volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。
问:这样的场景,我们在开发过程中都是使用ConcurrentHashMap,