详解HashMap、HashTable、ConcurrentHashMap、HashSet的异同

之前的文章《HashMap源码详解》中我们已经结合Java1.8中HashMap的源码对数据结构、数据存取、数据写入、扩容等操作进行了详细的梳理。

而HashMap又是HashSet、HashTable、ConcurrentHashMap这三种数据结构的基础。今天的文章我们就在《HashMap源码详解》的基础上,介绍HashSet、HashTable、ConcurrentHashMap的源码,并比较他们与HashMap的异同。

1 HashTable

HashTable和HashMap的关系最近,可以认为是HashMap的线程安全版本。

我们仍然以Java1.8为例,对HashTable进行分析后发现,其读、写、扩容等操作与HashMap基本一致,但是所有方法都增加了synchronized关键词的修饰,将其变为了同步方法。

1.1 源码分析

我们不再对所有的方法的源码进行一一分析,仅以put方法为例,进行介绍。其供外部公开调用的put方法如下:

public synchronized V put(K key, V value) {
    // 不允许插入null
    if (value == null) {
        throw new NullPointerException();
    }

    // 这里要确认要插入的元素不是已经存在在动态数组中
    Entry<?,?> tab[] = table;
    // 根据hash计算要插入元素的位置
    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;
}

对比《HashMap源码详解》中我们分析的HashMap的源码,我们主要发现了两点不同。

一是在进行真正插入前判断了要插入的元素在HashTable中不存在。在HashMap中其实也进行了对应的操作,只是将其放入了插入操作的子函数中,因此这点差异可以忽略。

二是在计算插入元素key的index时,相关的哈希值和位置计算并没有抽成一个子方法。这主要是:

  • 因为如果抽成一个同步子方法,那该子方法的操作频率非常高,会使得操作经常阻塞在这里,影响性能。
  • 如果抽成一个非同步子方法,则不同方法调用时可能导致并发问题。

因此,最好的办法就是每个方法中都写一遍,这是一种用空间换取性能的办法。

好了,我们继续看真正的插入方法:

private void addEntry(int hash, K key, V value, int index) {
    // 类似版本号
    modCount++;

    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

其中modCount类似于版本号,这是供悲观锁使用的。即如果在遍历过程中发现modCount改变,程序会报错。

count是存储元素的总数。用来作为扩容等操作的依据。

其他地方和HashMap操作一致。

1.2 对比

HashTable和HashMap的区别主要有:

  • HashMap是非线程安全的,HashTable是线程安全的。HashTable实现线程安全的办法是在方法上加同步锁,因此性能更差。
  • HashMap允许插入null值,而HashTable不允许。插入null时,HashTable会抛出NullPointerException。
  • HashMap默认初始化数组大小是16,HashTable的默认初始化数组大小是11。HashMap扩容容量变为2n,HashTable扩容时容量变为2n+1,这样元素分布更为均匀。

2 ConcurrentHashMap

因为HashTable是基于同步方法实现的线程安全,其效率很低,因此基本很少使用。而HashMap又不支持并发操作。那并发时大家都使用什么呢?就是我们这节所讲的ConcurrentHashMap。

HashTable中的同步方法实际上是对整个HashTable对象加锁,任何操作都会锁住整个对象。这样,当操作变多时,或者HashTable变大时,性能会很差。

而ConcurrentHashMap则采用了另外一种思路,它对整个数组进行了分段。然后对每一个小段进行同步保护,每次加锁只加给一小段数据加锁,那么只要多个操作分布在不同的段上,就可以安全地并发进行。因此提高了性能。

2.1 源码分析

ConcurrentHashMap的源码比较复杂,但是与HashMap的思路相似,再次基础上增加了分段(Segment),默认的分段数是16。也就是最多能够支持16个并发,即16个操作分别操作不同的段不会引发冲突和阻塞。而且,该分段数目一经初始化使用后,不允许在修改。

而每个分段内,则更像是一个HashMap。其初始化、扩容等操作都是针对于每个分段内而言的,每个分段内的数组独立扩容,大小可能各不相同,因此,整体而言ConcurrentHashMap是一组聚集在一起的HashMap。

而在进行插入、读取操作时,都是先找到对应的分段,然后在分段内进行操作。分段内的操作就类似于HashMap了,具体参考《HashMap源码详解》,我们不再重复讲解。

2.2 特点

我们对ConcurrentHashMap的特点进行总结:

  • 是线程安全的。并且内部采用分段加锁的策略,其效率比HashTable要高。
  • 和HashTable一样,不允许存入null值。

3 HashSet

为什么分析HashMap和相关Map却说道了HashSet?因为HashSet是基于HashMap实现的!

首先,我们看到HashMap中先引入了一个HashMap:

private transient HashMap<E,Object> map;

我们在HashSet中存入的值实际上是存入在了HashMap的key位置,而value处就填入下面的对象:

private static final Object PRESENT = new Object();

看一下HashSet的初始化方法:

public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<>(initialCapacity, loadFactor);
}

就是创建HashMap。所以其他的操作也都是基于HashMap进行的。我们就不再细讲了。

4 总结

本文我们在之前HashMap的基础上,分析了HashTable、ConcurrentHashMap、HashSet的源码,并总结了他们各自的特点:

  • HashTable是线程安全的。原理是在方法上加同步锁,因此性能更差。
  • ConcurrentHashMap是线程安全的。并且内部采用分段加锁的策略,其效率比HashTable要高。
  • HashSet是基于HashMap实现的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容