hashmap原理及与hashtable区别及ConcurrentHashMap同步实现

最近看了几家大公司的面试题目,发现hashmap出现的概率特别高,

好奇心使然,博主就想研究下它的原理,

查了很多资料,大概明白了,所以想自我总结下,以便与学习。

首先,先说下两种数据结构:数组,链表。

1.数组,存储地址连续,查找方便,但是插入删除操作相对于困难。

2.链表,存储地址不连续,分散,查找相对困难,但是插入删除操作相对方便

那有没有既查找方便又插入删除方便的数据结构呢?这时候,hashmap就出现了。

首先,给大家看下hashmap的存储示意图:


看了这个图别说大家,我都有点蒙,这是什么玩意?哈哈别着急,听我慢慢分析,

左边,很明显是一个size为16的数组。其中数组【0】的位置又存储了一个链表。

很明显hashmap是结合了数组和链表的优点做出来的数据结构。

那明眼人都发现了,这里有个entry是干什么的呢?

entry是一个java类,里面定义了key,value,next属性。数据就是存入这个类中的。

那么我们开始分析,hashmap怎样存数据的。

一.我们定义一个key。

通过int  hash= key.hashCode()得到key对应的hash值。

这里大家可能有问题hashCode()是什么?其实大家不用关心hashCode()这个函数,这个函数就是根据你的key,给你生成一个int型的散列值。

简单地说,你有个key,用hashCode()就可以生成一个int型的hash值。

二.拿到hash值之后,跟数组(假设a)的size做取模运算。这里数组长度16(默认16).

 hash%a.size()取到的值是他存储在数组的对应位置。

例如:11%16=11,

    27%16=11,

    43%16=11,

那么这三个key都保存在数组的第11位,也就是a[10].

假设第一个key1进来了a[10].key=key1,a[10].value=value1.

当第二个key2进来后,a[10].key=key2,a[10].value=value2,a[10].next.key=key1,a[10].next.value=value1.

就是这样,后进来的存在靠前的位置。

三.这里需要注意,key=null的数据要存在数组的第一个位置,也就是a【0】

分析完存数据,那么取数据也就容易了。

一.根据key得到key对应的hash值。

二.根据hash值,找到数据存放位置a[i],

三.遍历a[i]处的所有entry类,如果key值相等,则得到value.

怎么样?是不是很简单?

接下来分析下hashmap和hashtable的区别

首先,hashtable是同步的,也就是说,他是线程安全的,同时也意味着,单线程的时候,每次只能等一个线程处理完事情,释放同步锁,其他线程才可以处理。

所以速度也相应地变慢了。而hashmap是异步的。

其次,hashmap可以存储null值的key和value,hashtable不可以。

所以在单线程,不需要同步的时候,用hashmap。

追求安全性,用hashtable。


好的,就到这里,以上属于博主自我总结,有不对的地方望大家指正。拜了个拜^_^!

----------------------------------------------------------------------------------------------------------------------------------

2018.02.01追加

ConcurrentHashMap 是 util.concurrent 包的重要成员。本文将结合 Java 内存模型,分析 JDK 源代码,探索 ConcurrentHashMap 高并发的具体实现机制。

由于 ConcurrentHashMap 的源代码实现依赖于 Java 内存模型,所以阅读本文需要读者了解 Java 内存模型。同时,ConcurrentHashMap 的源代码会涉及到散列算法和链表数据结构,所以,读者需要对散列算法和基于链表的数据结构有所了解。

Java 内存模型

由于 ConcurrentHashMap 是建立在 Java 内存模型基础上的,为了更好的理解 ConcurrentHashMap,让我们首先来了解一下 Java 的内存模型。

Java 语言的内存模型由一些规则组成,这些规则确定线程对内存的访问如何排序以及何时可以确保它们对线程是可见的。下面我们将分别介绍 Java 内存模型的重排序,内存可见性和 happens-before 关系。

重排序

内存模型描述了程序的可能行为。具体的编译器实现可以产生任意它喜欢的代码 -- 只要所有执行这些代码产生的结果,能够和内存模型预测的结果保持一致。这为编译器实现者提供了很大的自由,包括操作的重排序。

编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。

重排序类型包括:

  • 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。
  • 处理器可以乱序或者并行的执行指令。
  • 缓存会改变写入提交到主内存的变量的次序。

内存可见性

由于现代可共享内存的多处理器架构可能导致一个线程无法马上(甚至永远)看到另一个线程操作产生的结果。所以 Java 内存模型规定了 JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。

在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程 A 写入一个变量值 V,随后另一个线程 B 读取变量 V 的值,在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:

  • 执行线程 A 的处理器把变量 V 缓存到寄存器中。
  • 执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去。
  • 执行线程 B 的处理器的缓存中有变量 V 的旧值。

Happens-before 关系

happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。

下面介绍几个与理解 ConcurrentHashMap 有关的 happens-before 关系法则:

  1. 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每动作 A 都 happens-before 于该线程中的每一个动作 B。
  2. 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
  3. Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
  4. 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C。

ConcurrentHashMap 的结构分析

为了更好的理解 ConcurrentHashMap 高并发的具体实现,让我们先探索它的结构模型。

ConcurrentHashMap 类中包含两个静态内部类 HashEntry 和 Segment。HashEntry 用来封装映射表的键 / 值对;Segment 用来充当锁的角色,每个 Segment 对象守护整个散列映射表的若干个桶。每个桶是由若干个 HashEntry 对象链接起来的链表。一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组。

HashEntry 类

HashEntry 用来封装散列映射表中的键值对。在 HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value 域被声明为 volatile 型。

清单 1.HashEntry 类的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
static final class HashEntry< K ,V> {
        final K key;                       // 声明 key 为 final 型
        final int hash;                   // 声明 hash 值为 final 型
        volatile V value;                 // 声明 value 为 volatile 型
        final HashEntry< K ,V> next;      // 声明 next 为 final 型
 
        HashEntry(K key, int hash, HashEntry< K ,V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }
}

在 ConcurrentHashMap 中,在散列时如果产生“碰撞”,将采用“分离链接法”来处理“碰撞”:把“碰撞”的 HashEntry 对象链接成一个链表。由于 HashEntry 的 next 域为 final 型,所以新节点只能在链表的表头处插入。 下图是在一个空桶中依次插入 A,B,C 三个 HashEntry 对象后的结构图:

图 1. 插入三个节点后桶的结构示意图:
图 1. 插入三个节点后桶的结构示意图:

注意:由于只能在表头插入,所以链表中节点的顺序和插入的顺序相反。

Segment 类

Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶。

table 是一个由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

count 变量是一个计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。注意,之所以在每个 Segment 对象中包含一个计数器,而不是在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。

清单 2.Segment 类的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static final class Segment< K ,V> extends ReentrantLock implements Serializable {
        /**
         * 在本 segment 范围内,包含的 HashEntry 元素的个数
         * 该变量被声明为 volatile 型
         */
        transient volatile int count;
 
        /**
         * table 被更新的次数
         */
        transient int modCount;
 
        /**
         * 当 table 中包含的 HashEntry 元素的个数超过本变量值时,触发 table 的再散列
         */
        transient int threshold;
 
        /**
         * table 是由 HashEntry 对象组成的数组
         * 如果散列时发生碰撞,碰撞的 HashEntry 对象就以链表的形式链接成一个链表
         * table 数组的数组成员代表散列映射表的一个桶
         * 每个 table 守护整个 ConcurrentHashMap 包含桶总数的一部分
         * 如果并发级别为 16,table 则守护 ConcurrentHashMap 包含的桶总数的 1/16
         */
        transient volatile HashEntry< K ,V>[] table;
 
  • 3
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值