HashMap和ConcurrentHashMap详解

Hello大家好,国庆期间给大家来普及一下关于Hash表知识点。

我们日常最多使用的哈希表应该是HashMap,但是很多时候HashMap都被称为是不安全的,并且Hash表有着独特的扩容机制,扩容系数等。。很多小伙伴可能在日常写java的时候并没有听说过这些东西,今天博主给大家整理了一下关于Hash表的知识点,一起来看看吧!

HashMap的安全问题

我们都知道HashMap是线程不安全的,因为在某些多线程的环境下会出现各种各样的问题,例如JDK7的扩容死循环问题,JDK8的链表和树结构转换的死循环问题,所以我们在使用的时候一定要考虑是否会出现并发,这样才能保证程序的正常运行

HashMap在JDK8之前是链表加数组的方式、JDK8之后引入的红黑树

多线程环境下请使用HashTable、ConcurrentHashMap、Collections.sychronizedMap(需要包装的HashMap)

JDK7==>扩容死循环

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

 单线程下扩容方式:

        当前节点在新的Hash节点上看是否有别的节点:

             ①没有、直接插入

             ②有,指向那个节点。然后自己变成头结点

所以扩容一次,链表就要反转一次

 可以看出单线程下是没有问题的

多线程下的问题

分别两个线程都刚好进行扩容,当线程1准备开始扩容的时候突然被打断,线程2抢到CPU,进行扩容,此时线程1-2两个线程中都是1->2没错吧,然后线程2扩容,链表变成了3->2->1,此时线程1拿到CPU,进行扩容,

第一遍For循环(e = 3) --> 此时1->2->3;
3.next = null;
null = 3;
e = 2;

第二遍
2.next = 3;
2 = e;
e = 1;

--> 1.next = 2 2.next = 1;

因为此代码

while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }

JDK8的时候是红黑树的死循环问题,这里就不赘述了(其实是博主自己也没搞明白,2333)

所以在多线程的环境下请使用ConcurrentHashMap(强烈推荐)

CocurrentHashMap可以看作线程安全且高效HashMap,相比于HashMap具有线程安全的优势,相比于HashTable具有效率高的优势。

而ConcurrentHashMap在JDK7和JDK8中也是不同的

JDK7

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry数组组成,Segment存储的是链表数组的形式,如图所示。



从上图可以看出,ConcurrentHashMap定位一个元素的过程需要两次Hash的过程,第一次Hash的目的是定位到Segment,第二次Hash的目的是定位到链表的头部。两次Hash所使用的时间比一次Hash的时间要长,但这样做可以在写操作时,只对元素所在的segment加锁,不会影响到其他segment,这样可以大大提高并发能力。

JDK8

 1.8在链表长度超过8并且表长度大于64的时候会变成红黑树来保证时间复杂度平衡

ConcurrentHashMap的缺点

因为保证效率每次只会锁部分数据,而并不会锁住整个表,读取也不会保证读取到最新,只能保证读取到已经顺利插入的数据。(这玩意很像数据的可重复读)

这里我使用CountDownLatch来等待线程结束后-->提交后

然后发现在被修改的时候依旧可以查询到数据-->不过是提交前的

这就有可能会导致数据不一致性问题(弱一致性)-->可以通过上锁来解决Sychronized/CAS

public class ConcurrentHashMapAndHashMap {

    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) {
        ConcurrentHashMap<Integer, Integer> concurrentHashMap = new ConcurrentHashMap<>();
        try {
            //ConcurrentHashMap是不能传入空的
            concurrentHashMap.put(1, 1);
            concurrentHashMap.put(2, 2);
            concurrentHashMap.put(3, 3);
            System.out.println("未修改时查询: ");
            for (Map.Entry<Integer, Integer> entry: concurrentHashMap.entrySet()) {
                System.out.println(entry.getKey() + " -- " + entry.getValue());
            }
        }catch (NullPointerException e) {
            System.out.println("空指针");
        }

        new Thread(()-> {
            concurrentHashMap.put(2, 15);
            try {
                Thread.sleep(2000);
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        System.out.println("修改时查询: ");
        for (Map.Entry<Integer, Integer> entry: concurrentHashMap.entrySet()) {
            System.out.println(entry.getKey() + " -- " + entry.getValue());
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("修改后查询: ");
        for (Map.Entry<Integer, Integer> entry: concurrentHashMap.entrySet()) {
            System.out.println(entry.getKey() + " -- " + entry.getValue());
        }

    }

}

 ConcurrentHashMap为了保证线程安全还不允许插入Null值

ConCurrentHashMap中的keyvaluenull会出现空指针异常,而HashMap中的keyvalue值是可以为null的。

原因如下:

ConCurrentHashMap是在多线程场景下使用的,如果ConcurrentHashMap.get(key)的值为null,那么无法判断到底是key对应的value的值为null还是不存在对应的key值。

而在单线程场景下的HashMap中,可以使用containsKey(key)来判断到底是不存在这个key还是key对应的value的值为null

在多线程的情况下使用containsKey(key)来做这个判断是存在问题的,因为在containsKey(key)ConcurrentHashMap.get(key)两次调用的过程中,key的值已经发生了改变。

就比如我们默认用get方法来取值,没有这个值取出来是Null,但是如果你插入这个Null数据然后查询的时候因为其他线程抢到CPU直接把数据Remove之后你再查还是Null,这个时候你就不知道这个数据到底是存在还是不存在了

HashMap的扩容机制

每次HashMap扩容都是扩容2倍,

原因:

1、因为是数组加链表-->所以我们插入每个数据都要进行取模,因为Hash表的大小始终为2的n次幂,因此可以将取模转为位运算操作,容量n为2的幂次方,n-1的二进制都变成1,这个时候可以充分散列,减少hash碰撞。

扩容代码

newTab[e.hash & (newCap - 1)] = e;

2、是否移位,由扩容后表示的最高位是否1为所决定,并且移动的方向只有一个,即向高位移动。因此,可以根据对最高位进行检测的结果来决定是否移位,从而可以优化性能,不用每一个元素都进行移位。
 

HashMap扩容因子

hashMap的扩容因子是0.75,因为符合泊松分布,这个大小的时候扩容hash碰撞是最小的,

加载因子过高,例如为1,虽然减少了空间开销,提高了空间利用率,但同时也增加了查询时间成本;

加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。

正好讲到了Hash碰撞

常用的解决hash碰撞的方法有

1、拉链法 --> 变成链表串起来

2、开放定址法-->

①:线性探查法:就是当前位置有值了就向右移动直到找到没有碰撞的位置

②:线性补偿探测法:di=Q 下一个位置满足 Hi=(H(key) + Q) mod m i=1,2,...k(k<=m-1) ,要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。

缺点:这种方法在冲突严重的时候会影响查找删除的效率

3、Rehash

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMapHashMap都是Java中常用的Map数据结构,但它们在实现和使用上有一些不同。 ConcurrentHashMap在JDK 1.7中的实现结构包括Segment(锁数组)、hashEntry(哈希数组)和链表(hashEntry节点)。Segment是对整个桶数组进行了分段,每个Segment都维护了一个独立的锁,这样不同的线程可以同时访问不同的Segment,从而提高了并发性能。hashEntry是存储实际键值对的数组,而链表则是用来解决哈希冲突的。 而HashMap则没有对整个桶数组进行分段,也没有使用锁来保证并发安全。它的底层实现结构只包括hashEntry和链表。在单线程环境下,HashMap的性能通常比ConcurrentHashMap好,因为它没有额外的并发控制开销。 当多个线程同时对ConcurrentHashMap进行操作时,每个线程只需要获取自己所对应的Segment的锁,这样不同线程之间的并发性能更高。而HashMap在多线程环境下,如果没有外部同步控制,可能会出现数据不一致的情况。 引用提供了ConcurrentHashMap在JDK 1.7中的实现结构。 引用提供了使用new()方法创建ConcurrentHashMap的示例。 引用提到了HashMap没有对整个桶数组进行分段的特点。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [HashMapConcurrentHashMap](https://blog.csdn.net/jdk819/article/details/119846649)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [ConcurrentHashMapHashMap的区别](https://blog.csdn.net/qq_46130027/article/details/130905598)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值