HashMap、Hashtable、ConcurrentHashMap的区别

1.概述

HashMap、Hashtable、ConcurrentHashMap是日常开发中使用频率较高的数据结构,它们都是以key-value的形式来存储数据,且都实现了Map接口,日常开发中很多人对其三者之间的区别并没有十分清晰的概念。本文将剖析部分源码,以及从线程安全性、速度等方面来分析三者之间的区别。首先讲下三者的一些区别:

1.HashMap与Hashtable基本上等价,区别在于Hashtable的大部分方法都是被synchronized修饰,并且键值都不能为null(HashMap则可以);
2.由于Hashtable大部分方法被synchronized修饰,因此是线程安全的,HashMap则是非线程安全的,大量的线程存取可能会出现异常;
3.hashMap效率相对比Hashtable高,因为synchronized修饰方法,获取锁会耗费时间,导致效率相对较低。

2.源码分析

2.1 HashMap

在这里插入图片描述
HashMap在jdk 1.7和jdk 1.8的版本在设计思想上有所改变,1.7主要是数组+链表,1.8是数组+链表+红黑树,红黑树也是一种链表数据结构。红黑树具有二叉树的优势,在查找方面具有一定优势,弥补jdk 1.7中HashMap因数据量较大导致链表过长、查询缓慢的问题。本文对于HashMap的源码分析主要基于jdk 1.7,因为jdk 1.7中源码相对容易理解。

2.1.1 为什么HashMap 的键值则都可以为 null?

这是因为hashMap在对键值为空的时候做了特殊处理,具体见下面源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

由上述代码可知,当key为空时,哈希时会直接被赋值为0。

2.1.2 HashMap属性说明

    // 初始化容量大小
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    //默认的加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75F;
    //table:数据存储区
    transient HashMap.Entry<K, V>[] table;
    //已存数据的大小
    transient int size;
    //table数组需要扩容的临界值,等于table的长度*loadFactor
    int threshold;
    //装载因子
    final float loadFactor;
    //table结构修改的次数
    transient int modCount;

2.1.3 为什么HashMap数组的大小总是2的n次方?

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
	static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
       return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

由上述代码可知,tableSizeFor方法就是扩容方法,所以你看得懂上述这个方法的操作么?稍微举个例子讲述一下:

1.当传入的cap值为35时;
2.首先执行第一步 cap-1,得到n的值为343.执行第二部:n或(n右移一位),34 二进制为:100010,n右移一位变成:010001,或操作结果为:1100114.n右移两位变成:001100,再与110011进行或操作,变成111111,此时n为1111115.n右移四位变成:000000,再与111111进行或操作,结果n仍为:1111116.n右移16为,再与111111进行或操作,n仍为1111117.n+1变成1000000,变成64

其实要看懂上述代码,首先要理解什么是或操作(或操作是一种由高位决定位值得运算,只要对应位有一个1,则进行与操作时该位便为1,只有对应位同时为0时,结果才为0)。所以上述代码每右移多少位,就把最高位右边的第x位设置为1。

2.2 Hashtable

在这里插入图片描述

2.2.1 Hashtable为什么不允许键值为null?

首先看一下Hashtable的put方法源码:

public synchronized V put(K key, V value) {
        // 确保value不为空。这句代码过滤掉了所有value为null的键值对
        if (value == null) {
            throw new NullPointerException();
        }
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        //在此处计算key的hash值,如果此处key为null,则直接抛出空指针异常。
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        addEntry(hash, key, value, index);
        return null;
    }

2.2.2 Hashtable为什么安全

在这里插入图片描述
从上图源码可以看出,hashtable的大部分方法都是由关键字synchronized修饰,因此是线程安全的。

2.3 ConcurrentHashMap

在这里插入图片描述

2.3.1 ConcurrentHashMap底层基于什么?

ConcurrentHashMap底层是基于数组+链表,而在jdk1.7和jdk1.8中稍有不同,jdk1.7中的数据结构采用分段式设计,segment数组 + HashEntry数组 + 链表实现,hash冲突采用拉链法处理。而在jdk1.8中,借鉴了jdk1.8中HashMap的设计思想,采用数组 + 链表 + 红黑树的数据结构,并且有原来的分段式锁换成了CAS + Synchronized锁,其它的地方并没有改变。

2.3.2 ConcurrentHashMap如何保证高并发?

static final class Segment<K,V> extends ReentrantLock implements Serializable {
       
        private static final long serialVersionUID = 2249069246763182397L;

        static final int MAX_SCAN_RETRIES =
            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
            
        transient volatile HashEntry<K,V>[] table;

       
        transient int count;

        //table结构修改的次数
        transient int modCount;

        //阈值
        transient int threshold;
        
		//加载因子
        final float loadFactor;

        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
   }

上述Segment类是ConcurrentHashMap的一个内部类,它是ConcurrentHashMap分段锁实现的基础,在生成一个ConcurrentHashMap 对象时,内部会维护一个Segment数组,这个Segment数组会将一个大的table分割成多个小的table来进行加锁。从理论上说,segment数组的数量是多少,并发量就是多少。

public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }

对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置。当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

2.4 如何保证HashMap线程安全

1.使用Collections.synchronizedMap(Map)创建线程安全的map集合;
2.使用Hashtable替代hashMap;
3.使用ConcurrentHashMap来代替hashMap;

3.小结

1.hashMap、hashtable的主要区别在于安全性、同步性、速度,要根据场景来选择不同的数据结构;
2.hashMap的键合值都可以为空,但是hashtable、ConcurrentHashMap的键值都不可以为空;
3.hashtable、ConcurrentHashMap都是线程安全的,hashMap是非线程安全的;
4.hashtable是一个过时的类,使用线程安全的ConcurrentHashMap。

4.参考文献

1.https://juejin.cn/post/6844904023003250701
2.https://developer.aliyun.com/article/38213?spm=a2c6h.14164896.0.0.13f64a13sROcRL
3.https://zhuanlan.zhihu.com/p/69284871

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值