Java-并发篇-05-Map集合不安全问题

1. 简述

HashMap是我们非常常用的数据结构,由数组和链表组合构成的数据结构。
安全集合一般使用ConcurrentHashMap;

1.1 ConcurrentHashMap1.7与1.8

ConcurrentHashMap当中,在Java7叫Entry,在Java8中叫Node。
java8之前是头插法,在java8之后,改用尾部插入了。

1.2 为啥改为尾部插入呢?

数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容,也就是resize。
什么时候resize呢?
有两个重要的元素:
Capacity:HashMap当前长度(也就是数组加链表上所装的元素个数)。
LoadFactor:负载因子,默认值0.75f。

1.7的时候会判断大小是否超过阈值以及该元素计算的下标位置是否为null
1.8的时候就不会判断该元素计算的下标位置是否为null

1.3 扩容?它是怎么扩容的呢?

扩容:创建一个新的Entry空数组,长度是原数组的2倍。
ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

1.4 为什么要重新Hash呢,直接复制过去不香么?

是因为长度扩大以后,Hash的规则也随之改变。
Hash的公式—> index = HashCode(Key) & (Length - 1)

1.5 为啥之前用头插法,java8之后改成尾插了呢?

因为resize的赋值方式,也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置,
在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
一旦几个线程都调整完成,就可能出现环形链表。
悲剧就出现了——Infinite Loop。
使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
Java7在多线程操作HashMap时可能引起死循环,
原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系。
Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。

那是不是意味着Java8就可以把HashMap用在多线程中呢?

我认为即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,
多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,所以线程安全还是无法保证。

看源码的时候初始化大小是16,为啥是16不?
在JDK1.8的 236 行有1<<4就是16,

1.6 为啥用位运算呢?直接写16不好么?

这样是为了位运算的方便,位与运算比算数计算的效率高了很多,之所以选择16,是为了服务将Key映射到index的算法。
所有的key我们都会拿到他的hash,但是我们怎么尽可能的得到一个均匀分布的hash呢?
是的我们通过Key的HashCode值去做位运算。
index的计算公式:index = HashCode(Key) & (Length- 1)
在使用不是2的幂的数字的时候,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。
只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
这是为了实现均匀分布。

那我问你个问题,
为啥我们重写equals方法的时候需要重写hashCode方法呢?
1.使用hashcode方法提前校验,可以避免每一次比对都调用equals方法,提高效率
2.保证是同一个对象,如果重写了equals方法,而没有重写hashcode方法,会出现equals相等的对象,hashcode不相等的情况,重写hashcode方法就是为了避免这种情况的出现。
你能用HashMap给我举个例子么?
所有的对象都是继承于Object类。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。
在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样
对于值对象,==比较的是两个对象的值
对于引用对象,比较的是两个对象的地址

大家是否还记得我说的HashMap是通过key的hashCode去寻找index的,那index一样就形成链表了,
也就是说”熊大“和大熊“的index都可能是2,在一个链表上的。
去get的时候,他就是根据key去hash然后计算出index,找到了2,那我怎么找到具体的”熊大“还是”大熊“呢?
equals!是的,所以如果我们对equals方法进行了重写,
建议一定要对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。
不然一个链表的对象,你哪里知道你要找的是哪个,到时候发现hashCode都一样,这不是完犊子嘛。

位运算(&)效率要比取模运算(%)高很多主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因为处理非常快。
**默认容量为什么是16,**可能原因:太小就有可能频繁扩容,太大浪费空间,16作为一个经验被采用了。
当我们自定义初始化容量的时候,Hsahmap不一定会直接采用我们传的数值,而是经过计算,得到一个新的值,
目的是提高hash效率 eg:1->1 3->4 7->8 9->16
在jdk1.7和1.8当中,HashMap初始化这个容量的时机不同,1.7创建的时候不会初始化,采用的是懒加载机制,在put的时候会
进行扩容,1.8在调用hashMap的构造函数定义HashMap的时候,会进行容量的设定。
负载因子设定为0.75f的好处,那就是0.75正好是4分之3,而capacity又是2的幂,两者乘必为整数

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alan0517

感谢您的鼓励与支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值