马桶Java:2.HashMap基本代码讲解 JDK1.7 存在的问题

马桶🚽Java 上厕所就能看完的小知识! 欢迎关注、点赞 持续更新!

JAVA HASHMAP的死循环引用以上文中的部分片段

HashMap 与 哈希表

首先我们可以给定我们需要存储的各种元素生成一个我们需要的Hash值。
按照int范围计算那我们可能会生成 -231—231-1的哈希值数
我们不能让他们每一个都单独存防这样数量很大
所以我们出现了哈希桶。那什么是哈希桶? 桶是什么?
桶可以用一个很简单的例子:我们现在有10个数,我们将对其进行储存。那么我们可以创建两个桶,每个桶装5个数 【0-5】【6-10】 每个组就可以称为一个桶。
所以哈希桶就是将一定范围的hash值装在一起存储。桶的底层是数组。
使用数组的原因是因为硬件电路的线性地址变换可以直接找到时间复杂度为O(1) 和 数量无关
说白了 就是快!

哈希之间的关系

就因为链表问题才留下了Hashmap出现的安全隐患

HashMap Jdk1.7源码解读

首先Jdk1.7版本的就是上图中经典的哈希实现方法 哈希桶+拉链法
看源码我们首先要看其构造函数

  /*
     *在构造函数中未指定时使用的负载系数。
     */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;
   /*
    构造一个空的{@code HashMap},其默认初始容量(16)和默认加载因子(0.75)。
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他所有字段默认
    }

当我们创建Hashmap时并没有创建容器 只是明确了默认的负载因子为0.75
创建容器在put方法被调用的时候

负载因子

如果table[]的尺寸很小,比如只有2个,如果要放进10个keys的话,那么碰撞非常频繁,于是一个O(1)
的查找算法,就变成了链表遍历,性能变成了O(n),这是Hash表的缺陷

比如说我们提供了一个初始容量为16,那么当我们将数组中(哈希桶)所有的桶都装满时一定存在着有的数组元素已经开始出现链表,甚至可能很长。如果hash算法实现的不够好那么可能就会出现退化,变成链表。我们知道链表的时间复杂度是O(n)。就会特别影响性能。因此我们需要扩容
这个时候我们就要提供一个标准。用初识容量 * 负载因子当到底这个值时。
我们就会扩容来满足存储保证其性能。否则出现过长链表就会退化,变成链表结构。

负载因子为什么选择0.75?
官方解释是:
默认负载因子(.75)在时间和空间成本之间提供了一个很好的折衷方案。
较高的值会减少空间开销(因为不需要扩容),但会增加查找成本(链表中的值过多)在HashMap类的大多数操作中都得到了体现

Put方法 扩容

默认容量是如下定义的

默认的初始容量必须为2的幂。
 /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

为什么为2的幂?这里我先卖个关子。一会随着知识的不断讲解我们就会知道
首先:put()节选

public V put(K key, V value)
{
// 如果 table为空 则创建一个给定大小的容器 如果你没给 那么默认为16
 if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    ......
    //计算算Hash值
    int hash = hash(key.hashCode());
    // 计存放在数组中的位置
    int i = indexFor(hash, table.length);
    //如果该key已被插入,则替换掉旧的value (链接操作)
    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;
        }
    }
    modCount++;
    //该key不存在,需要增加一个结点 (链表操作)
    addEntry(hash, key, value, i);
    return null;
}

为什么需要 2的幂数呢 因为当我们进行int i = indexFor(hash, table.length);方法时。会有一条这样的操作

int =  hash & (length-1);

如果是2n - 1 进行二进制换算是可以保证全是1的结果
然后我们和hash值按位与& 可以计算出一个数。这个数的结果就是他所在数组中的位置

如果不是2n 那我们无法保证:二进制换算结果全为1 如果出现0那么与hash值得按位与&换算肯定有一位永远为0,即可能永远有那么几个桶没有保存任何数据。无法保证数据被均匀保存在各个桶中

例如:2(n) - 1  换算结果为10000-1 = 1111
那么这个结果和hash值(0101111....1001)进行按位与 的结果就是 1001 即数组下标

如果不是 2(n) 那么有可能换算结果为1001。那么无论与什么hash值进行按位与运算 中间的两位永远为0 那么就会有几个桶永远没有数据

当我们需要添加新节点的时候 就会进行判断是否已经超出了预设置的阈值(初始容量 * 负载因子)
如果超过了则需要进行 resize()
这个方法 就是 jdk 1.7版本HashMap出现问题的关键

void addEntry(int hash, K key, V value, int bucketIndex)
{
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
    if (size++ >= threshold)
        resize(2 * table.length);
} 

因此我们需要一张新表 将老表的Hash表迁移到新表上 扩容为二倍
因为桶的数量增加 我们需要对所有存储元素进行rehash 重新排列

void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

迁移代码如下 记住标记数字行的代码

void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面这段代码的意思是:
    //  从OldTable里摘一个元素出来,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
  1              Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
  2               e.next = newTable[i];
  3              newTable[i] = e;
  4              e = next;
            } while (e != null);
        }
    }
}

多线程的时候使用Hashmap 就可能造成如下情况 形成链表的无限循环
获取的时候如果需要获取后面的元素就会造成cpu100%
具体内容想了解的可以查看如下JAVA HASHMAP的死循环

image.png

但是HashMap的注释中明确写着
HashMap类与Hashtable类似,但它是不同步的,并且允许为null。
因为HashMap本来就不支持并发。要并发就用ConcurrentHashMap

安全问题

使用Hash碰撞进行DoS攻击

image.png

哈希表碰撞攻击就是通过精心构造数据,使得所有数据全部碰撞,人为将哈希表变成一个退化的单链表。
此时哈希表各种操作的时间均提升了一个数量级,因此会消耗大量CPU资源,导致系统无法快速响应请求,从而达到拒绝服务攻击(DoS)的目的。
因为Java中的String 转hash值方法是可见的。
因此会有人组建大量的字符串进行网络传递的方式传递给服务器 。
Tomcat Oracle 都是使用HashMap进行http请求储存 那么就有可能造成hash碰撞攻击
当时tomcat社区曾提交过这个问题 。后来在1.7版本中 HashMapString字符串的转化hash值 又使用了另一种hash算法进行两次hash。 而这种算法据说性能不高。

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

推荐阅读更多精彩内容