对于hashmap的一点理解
对于JDK1.8之后的hashMap,底层采用数组+链表、红黑树的实现方式。
我们都知道对于给定的key先计算其hashcode然后hashcode再对数组的长度取余从而得到其所在的数组下标,当出现hash冲突时从,采用的链地址法将相同位置的Entry串到一条链表上,但是随着链表长度的增大,会极大的降低查找速率,因此当链表长度大于8时会由链表转为红黑树。(使用红黑树而不直接使用经典的AVL树的原因是红黑树与AVL树查询效率相当,但是红黑树牺牲一部分的平衡性从而提高了插入删除的效率,总体效率得到提升)。
1.7中的的hash算法很容易构造出hash值相等的key,产生长链表,使用如此大量的key可以对服务器进行大量请求,并进一步进行dos攻击。
1.7中的扩容会带来死循环问题。
巧用位运算
给定一个key其所在的数组下标的计算:
index = hashcode & (n - 1)
上述式子中n指的是当前数组的长度,其值必须为2的整数次幂。
hashcode对长度取余的操作是通过hashcode与n - 1的与运算实现的,例如n = 16 hashcode = 20,取余等于4
n = 1 0 0 0 0
n - 1 = 0 1 1 1 1
hashcode = 1 1 0 0 0
hashcode & (n - 1) = 0 1 0 0 0
通过该例子我们发现了,当n是2的整数次幂的时候,其-1的值就为后面一个连0串+连1串,与该值相与的结果就恰好为对n取余的结果。
此外jdk1.8之后hashcode的计算变为如下
hashcode = hashcode ^ (hashcode >>> 16)
如此为了处理hashcode高位不同低位相同产生hash冲突的情况。
例如N = 2^16 ,若使用旧的计算方式,当低16相同时对于高16位取任意值其hashcode总是相同的
扩容带来的线程安全问题
当前的桶数目达到最大数组*0.75之后时,会进行扩容操作,每次增加一倍。
除了一般线程安全问题,hashmap的扩容还存在一个致命的线程问题。
扩容过程为首先创建一个新的数组,再对旧数组的结点重新计算数组下标(毕竟数组长度变了),然后复制过去。
1.7中的扩容会带来死循环问题。其复制源码如下
// jdk1.7 HashMap
void transfer(Entry[] newTable)
{
// oldTable
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
// 将旧位置置null
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
上述过程就是一个链表的头插法的过程。第一步保存其后一个结点,第二步将该节点插入到新表的头部,第三步换头
单线程操作毫无问题,并发操作时当第一个线程刚执行完第一步时,第二个线程抢占了处理机,然后完成整个过程。
上述第一张图为线程一刚执行完第一步,第二张图为线程2抢占完完成整个过程后线程一执行第二步,第三张图为线程1执行完第三步的结果。
jdk1.8之后使用尾插法的方式解决该问题。那么jdk1.7版本中为何不使用尾插法呢?由于jdk1.7版本中不存在红黑树只有链表结点,如果都采用尾插法会极大地降低效率,此外hashMap并不是线程安全的容器,多线程场景下还是采用concurrentHashMap。
document.querySelectorAll('.github-emoji') .forEach(el => { if (!el.dataset.src) { return; } const img = document.createElement('img'); img.style = 'display:none !important;'; img.src = el.dataset.src; img.addEventListener('error', () => { img.remove(); el.style.color = 'inherit'; el.style.backgroundImage = 'none'; el.style.background = 'none'; }); img.addEventListener('load', () => { img.remove(); }); document.body.appendChild(img); });
- dom4j的读写xml文件,读写xml字符串
- 省市县三级联动
- 从国家统计局爬下来的地区信息
- app令牌的一个token实现
- velocity分页模板
- js基础-表单验证和提交
- No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK?
- oracle创建用户
- oracle创建表相关
- spring学习遇到的问题汇总
- Java XML解析工具 dom4j介绍及使用实例
- redis学习教程之一基本命令
- 在java中使用redis
- springmvc学习笔记--json--返回json的日期格式问题
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- java之单例设计模式
- Nginx | Nginx的介绍和安装
- 真特么激动第一个爬虫----爬取豆瓣电影top250
- LeetCode | 141.环形链表
- LeetCode | 20.有效的括号
- springboot开发之实现登录功能
- springboot开发之国际化(中英文切换)
- LeetCode | 225.用队列实现栈
- springboot开发之显示员工信息
- LeetCode | 232.用栈实现队列
- springboot开发之thymeleaf页面公共元素的抽取
- Redis | Redis 通用命令
- python爬虫--自动下载cosplay小姐姐图片(xpath使用自定义创建文件路径)
- springboot开发之修改员工
- LeetCode | 703.数据流中的第K大元素