马桶🚽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的死循环
但是HashMap
的注释中明确写着
HashMap类与Hashtable类似,但它是不同步的,并且允许为null。
因为HashMap
本来就不支持并发。要并发就用ConcurrentHashMap
安全问题
使用Hash
碰撞进行DoS
攻击
哈希表碰撞攻击就是通过精心构造数据,使得所有数据全部碰撞,人为将哈希表变成一个退化的单链表。
此时哈希表各种操作的时间均提升了一个数量级,因此会消耗大量CPU资源,导致系统无法快速响应请求,从而达到拒绝服务攻击(DoS)的目的。
因为Java中的String 转hash值方法是可见的。
因此会有人组建大量的字符串进行网络传递的方式传递给服务器 。
如
Tomcat Oracle
都是使用HashMap
进行http
请求储存 那么就有可能造成hash碰撞攻击当时
tomcat
社区曾提交过这个问题 。后来在1.7版本中 HashMap
对String
字符串的转化hash
值 又使用了另一种hash算法进行两次hash。 而这种算法据说性能不高。
网友评论