聊聊HashMap & HashTable & ConcurrentHashMap等

参考和转自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在多线程环境下存在线程安全问题,那你一般都是怎么处理这种情况的?

答:一般在多线程场景,会使用好几种不同方式去代替:

  1. 使用Collections.synchronizedMap(Map)创建线程安全的map集合。
  2. Hashtable。
  3. 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,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值