关闭

HashMap的基本原理与它的线程安全性

556人阅读 评论(0) 收藏 举报
分类:

http://blog.csdn.net/t894690230/article/details/51323946

1. 前言

能用图说清楚的,就坚决不用代码。能用代码撸清楚的,就坚决不写解释(不是不写注释哦)。

以下所有仅针对JDK 1.7及之前中的HashMap。

2. 数据结构

HashMap内部通过维护一个Entry<K, V>数组(变量为table),来实现其基本功能,而Entry<K, V>是HashMap的内部类,其主要作用便是存储键值对,其数据结构大致如下图所示。

Entry的数据结构

从Entry的数据结构可以看出,多个Entry是可以形成一个单向链表的,HashMap中维护的Entry<K, V>数组(之后简称为Entry数组,或table,容易区分)其实就是存储的一系列Entry<K, V>链表的表头。那么HashMap中存储数据table数组的数据结构,大致可以如下图所示(假设只有部分数据)。

HashMap的数据结构

注:Entry数组的默认长度为16,负载因子为0.75。

将上图中的每一行,称为桶(bucket),那么table的索引便是bucketIndex。而HashMap中的插入、获取、删除等操作最主要的便是对table和桶(bucket)的操作。下面将主要通过插入操作,看其数据结构的变化。

3. 插入

对于上图中的数据结构,插入操作便是将要插入的键 - 值(key - value)对根据key计算hash值来选择具体的存储位置。

插入函数的源码如下(以Mark开头的或者中文注释,非JDK源码中的注释,下同):

public V put(K key, V value) {
    // Mark A Begin
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    // Mark A End

    int hash = hash(key); // 计算hash值
    int i = indexFor(hash, table.length); // 计算桶的位置索引(bucketIndex)

    // Mark B begin
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // Mark B end

    modCount++; // 记录修改次数,迭代的时候会据此判断是否有被修改
    addEntry(hash, key, value, i);
    return null;
}
  • 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
  • 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

在上面的代码中,代码段A(Mark A Begin - Mark A End,下同)的主要作用是如果table为空则初始化数组以及插入key为null时的操作,代码段B则是插入相同key时覆盖原有的值,并返回原有的值。这里重点关注的是addEntry(hash, key, value, i)方法。

addEntry方法源码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩充table数组的大小
        resize(2 * table.length);
        // 重新计算hash值
        hash = (null != key) ? hash(key) : 0;
        // 重新计算桶的位置索引
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

createEntry方法源码如下:

void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    // 将新的Enrty元素插入到对应桶的表头
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Entry<>实例化的源码如下:

Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n; // 将原先桶的表头向后移动
    key = k;
    hash = h;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在整个插入操作中,有一个很重要的操作,便是对table数组扩容,扩容的算法相对简单,但是在多线程下它却容易引发一个线程安全的问题。

注:扩容需要会把原先table中的值移动到新的数组中,再赋值给table变量,一个合适的初始大小和负载因子能够提高效率。

4. 线程不安全

在多线程环境下,假设有容器map,其存储的情况如下图所示(淡蓝色为已有数据)。

这里写图片描述

此时的map已经达到了扩容阈值12(16 * 0.75 = 12),而此时线程A与线程B同时对map容器进行插入操作,那么都需要扩容。此时可能出现的情况如下:线程A与线程B都进行了扩容,此时便有两个新的table,那么再赋值给原先的table变量时,便会出现其中一个newTable会被覆盖,假如线程B扩容的newTable覆盖了线程A扩容的newTable,并且是在A已经执行了插入操作之后,那么就会出现线程A的插入失效问题,也即是如下图中的两个table只能有一个会最后存在,而其中一个插入的值会被舍弃的问题。

这里写图片描述

这便是HashMap的线程不安全性,当然这只是其中的一点。而要消除这种隐患,则可以加锁或使用HashTable和ConcurrentHashMap这样的线程安全类,但是HashTable不被建议使用,推荐使用ConcurrentHashMap容器。


1
0
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

Hashtable,HashMap,ConcurrentHashMap 底层实现原理与线程安全问题

术语定义 术语 英文 解释 哈希算法 hash algorithm 是一种将任意内容的输入转换成相同长度输出的加密方式,其输出被称为哈希值。  ...
  • qq_27093465
  • qq_27093465
  • 2016-08-22 19:15
  • 9997

浅析HashMap与ConcurrentHashMap的线程安全性

本文要解决的问题: 最近无意中发现有很多对Map尤其是HashMap的线程安全性的话题讨论,在我的理解中,对HashMap的理解中也就知道它是线程不安全的,以及HashMap的底层算法采用了链地址法来...
  • sbq63683210
  • sbq63683210
  • 2016-06-15 11:14
  • 7858

【java并发】造成HashMap非线程安全的原因

0. 写在前面  在前面我的一篇总结线程范围内共享数据文章中提到,为了数据能在线程范围内使用,我用了HashMap来存储不同线程中的数据,key为当前线程,value为当前线程中的数据。我取的时候根据...
  • eson_15
  • eson_15
  • 2016-05-31 11:18
  • 7402

【图解JDK源码】HashMap的基本原理与它的线程安全性

1. 前言能用图说清楚的,就坚决不用代码。能用代码撸清楚的,就坚决不写解释(不是不写注释哦)。2. 数据结构HashMap内部通过维护一个Entry数组(变量为table),来实现其基本功能,而Ent...
  • t894690230
  • t894690230
  • 2016-05-05 18:06
  • 817

Android应用程序基本原理(3:进程与线程)

2.3  进程与线程当应用程序的第一个组件需要运行时,Android为它启动一个单线程执行的Linux进程。默认地,应用程序的所有组件运行在该进行和线程中。不过,可以安排组件运行在其他的进程中,并且你...
  • zeng622peng
  • zeng622peng
  • 2011-04-27 21:02
  • 923

线程、多线程基本原理与两种实现方法

多线程基本原理与两种实现方法
  • Leon_ang
  • Leon_ang
  • 2017-05-22 14:31
  • 240

线程重用——线程池的基本原理

为简单起见,线程池中只有一个线程:package com.xs.concurrent; import java.util.concurrent.BlockingQueue; import java....
  • zhangzeyuaaa
  • zhangzeyuaaa
  • 2015-10-19 11:32
  • 2638

Boost Asio 中的线程和基本原理

说到Boost.Asio的线程时,我们经常在讨论:  io_service:io_service是线程安全的。几个线程可以同时调用io_service::run()。大多数情况下你可能在一个单线程...
  • xiaominggunchuqu
  • xiaominggunchuqu
  • 2017-07-01 16:06
  • 402

Java HashMap笔记之一:基本原理

摘要: Java中的HashMap是一种简单易用而且高效强大的数据结构,在开发过程中经常使用。这里总结下HashMap的基本原理。HashMap默认内部数组大小?HashMap内部数组为16(JDK7...
  • x380481791
  • x380481791
  • 2017-07-29 09:24
  • 120

AsyncTask - 基本原理 后台线程和UI线程的交互

前面一个文章大概描述了一下任务是怎么被执行的?http://blog.csdn.net/zj510/article/details/51485120 其实也就是AsyncTask里面的doInBac...
  • zj510
  • zj510
  • 2016-05-24 13:30
  • 600
    个人资料
    • 访问:9060956次
    • 积分:75839
    • 等级:
    • 排名:第25名
    • 原创:262篇
    • 转载:2812篇
    • 译文:3篇
    • 评论:787条
    文章分类
    最新评论