hashmap扩容线程安全问题_HashMap源码解析,扩容机制及其思考

1.概述

HashMap是日常java开发中常用的类之一,是java设计中非常经典的一个类,它巧妙的设计思想与实现,还有涉及到的数据结构和算法,,值得我们去深入的学习。

简单来说,HashMap就是一个散列表,是基于哈希表的Map接口实现,它存储的内容是键值对 (key-value) 映射,并且键值允许为null(键的话只允许一个为null)。

1.1 注意事项

①根据键的hashCode存储数据。(String,和Integer、Long、Double这样的包装类都重写了hashCode方法,String比较特殊根据ascil码还有自己的算法计算,Double做位移运算【具体看源码的hashcode实现】,Integer,Long包装类则是自身大小int值),

HashMap中的结构不能有基本类型,一方面是基本类型没有hashCode方法,还有HashMap是泛型结构,泛型要求包容对象类型,而基本类型在java中不属于对象。

②HashMap的存储单位是Node,可以认作为节点。

③Hashmap中的扩容的个数是针对size(内部元素(节点)总个数),而不是数组的个数。比如说初始容量为16,第十三个节点put进来,不管前面十二个占的数组位置如何,就开始扩容。

1.2 hashmap几个特征

特征

说明

是否允许重复数据

key如果重复会覆盖,value允许重复

hashMap是否有序

无序,这里的无序指的是遍历HashMap的时候,得到的顺序大都跟put进去的顺序不一致

hashMap是否线程安全

非线程安全,因为里面的实现不是同步的,如果想要线程安全,推荐使用

键值是否允许为空

key和value都允许为空,但只允许一个为空

2.一些概念

2.1.位运算

位运算是对整数在内存中的二进制位进行操作。

在java中 >> 表示右移 若该数为正,则高位补0,若为负数,高位补1

<

例如20的二进制为0001 0100 20>>2为 0101 0000 结果为5(左高右低)

20<<2 为 0101 0000 则为80

java中>>>和>>的区别

>>>表示无符号右移,也叫逻辑右移。不管数字是正数还是负数,高位都是补0

在hashMap源码中有很多使用位运算的地方。例如:

//之所以用1 << 4不直接用16,0000 0001 -> 0001 0000 则为16,如果用16的话最后其实也是要转换成0和1这样的二进制,位运算的计算在计算机中是非常快的,直接用位运算表示大小以二进制形式去运行,在jvm中效率更高。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //初始化容量

注意:左移没有<<

2.2 位运算符-(与(&)、非(~)、,或(|)、异或(^))

①与运算(&)

我们都知道&在java中表示与操作&表示按位与,这里的位是指二进制位。都为1才为真(1),否则结果为0,举个简单的例子

System.out.println(9 & 8); //1&1=1,1&0 0&1 0&0都=0,因此1001 1000 -> 1000 输出为8

②非运算(~)

源码 -> 取反 -> 反码 -> 加1 -> 补码 -> 取反 -> 按位非值

在Java中,所有数据的表示方法都是以补码的形式表示,如果没有特殊说明,Java中的数据类型默认是int,int数据类型的长度是8位,一位是四个字节,就是32字节,32bit.

例如5的二进制为0101

补码后为 00000000 00000000 00000000 00000101

取反后为 11111111 11111111 11111111 11111010

【因为高位为1 所以源码为负数,负数的补码是其绝对值源码取反,末尾再加1】

所以反着来末尾减1得到反码然后再取负数

末位减1:11111111 11111111 11111111 11111001

【后八位前面4位不动 后面 减1 1010减1 相当于 10-1为9 后四位就是 1001 】

取反后再负数: 00000000 00000000 00000000 00000110 为-6

System.out.println(~ 5); //输出-6

③或运算(|)

只要有一个为1,结果为1,否则都为0

System.out.println(5 | 15); //输出为15,0101或上1111,结果为1111

④异或运算(^)

相同为0(假),不同为真(1)

System.out.println(5 ^ 15); //输出10 0101异或1111结果为1010

2.3 hashcode

hash意为散列,hashcode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值,顶级父类Object类中含hashCode方法(native本地方法,是根据地址来计算值),有一些类会重写该方法,比如String类。

重写的原因。为了保证一致性,如果对象的equals方法被重写,那么对象的hashcode()也尽量重写。

简单来说 就是hashcode()和equals()需保持一致性,如果equals方法返回true,那么两个对象的hashCode 返回也必须一样。

否则可能会出现这种情况。

假设一个类重写了equals方法,其相等条件为属性相等就返回true,如果不重写hashcode方法,那么依据就是Object的依据比较两个对象内存地址,则必然不相等,这就出现了equals方法相等但是hashcode不等的情况,这不符合hashcode的规则,这种情况可能会导致一系列的问题。

因此,在hashMap中,key如果使用了自定义的类,最好要合理的重写Object类的equals和hashcode方法。

2.4 哈希桶

哈希桶的概念比较模糊,个人理解是数组表中一块区域结果下面的单向链表组成的,在hashmap中,这个单向链表的头部是所在数组上第一个元素,单向链表如果过长超过8,那么这个"桶"就可能变成了红黑树(前提是数组长度达到64)。

2.5 hash函数

在程序设定中,把一个对象通过某种算法或者说转换机制对应到一个整形。

主要用于解决冲突的。

2.6 哈希表

也称为散列表,这也是一种数据结构,可以根据对象产生一个为整数的散列码(hashCode)。

hash冲突

HashMap之所以有那么快的查询速度,是因为他的底层是由数组实现,通过key计算散列码(hashCode)决定存储的位置,HashMap中通过key的hashCode来计算hash值,只要hashCode相同,hash值也一样,但是可能存在存的对象多了,不同对象计算出的hash值相同,这就是hash冲突。

举个例子

HashMap map = new HashMap();

map.put("Aa", "haha");

map.put("BB","heihei");

System.out.println("Aa".hashCode()); //2112

System.out.println("BB".hashCode()); //2112

//这里的Aa和BB为String型,String类重写了hashCode方法(根据ascil码和特定的算法来计算,虽然很巧妙但也难以避免不对对象hashCode相同的情况),Aa和BB的hashCode值相同,相同的HashCode的hash值相同

//根据源码就算key不相同 但key.hashCode()相同 则会返回相同的hash,导致hash冲突

static final int hash(Object key) {//取关键key的hash值

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//任何小于2的16次方的数 右移16位都为0 2的16次方>>>16刚好为1 任何一个数和0按位异或都为这个数本身(1和0为1 0和0为0),所以这个hash()函数对于null的hash值 仅在hashcode大于2的16次方才会调整值,这边16设计的很巧妙,因为int刚好是32位的取中间位数

}

2.7 二叉查找树和红黑树

红黑树是一种自平衡二叉查找树。是一种数据结构,又称二叉b树,(→_→ 2b树?),红黑树本质上也是二叉查找树。所以先理解下二叉查找树。

2.7.1二叉查找树

二叉查找树,又称有序二叉树,已排序二叉树

它的三大特点如下

1.左子树上所有结点的值均小于或等于它的根结点的值。

2.右子树上所有结点的值均大于或等于它的根结点的值。

3.左、右子树也分别为二叉排序树。

二叉树.png

2.7.2 红黑树(RBTree)

由于二叉查找树可能存在难以平衡呈线性的缺陷,所以出现的红黑树的概念。顾名思义,红黑树是只有红色和黑色节点的二叉树。

它的5大性质如下。

1.节点是红色或黑色。

2.根节点是黑色。

3.每个叶子节点都是黑色的空节点(NIL节点)。

4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

简单来说红黑树是一种自平衡二叉查找树,相比于普通的二叉查找树,它的数据结构更为复杂,但是在复杂的情况也能通过自平衡(变色,左右旋转)保持良好的性能。

关于红黑树,很形象的一组漫画,查看这里

在线模拟红黑树增删的地址地址1、 地址2

红黑树的时间复杂度为【吐槽下简书这边如果用数学公式太蛋疼了】:

O(logn)

它的高度为:[logN,logN+1](理论上,极端的情况下可以出现RBTree的高度达到2logN,但实际上很难遇到)。*

此外,由于它的设计任何不平衡将在三次旋转内解决。

红黑树和avl树(最早的自平衡二叉树)的比较:

avl更加平衡,查询速率稍强于红黑树,但是插入和删除红黑树完爆avl树,可能由于hashMap的增删也挺频繁的,所以综合考虑而选择红黑树。

总结:红黑树是种可以通过变色旋转的自平衡二叉查找树,对于hashMap来说,使用红黑树的好处在于,当有多个元素hash相同在同一数组下标的时候,使用红黑树在查找这些hash冲突的元素更快,它的时间复杂度从遍历链表O(n)降到O(logN)。

2.8 复杂度

算法复杂度分时间复杂度和空间复杂度。

时间复杂度:执行算法所需要的计算工作量

空间复杂度:执行算法所需要内存空间大小

时间和空间都是计算机资源的体现,算法的复杂性体现在运行该算法时计算机所需资源的大小。

这里重点讲下时间复杂度

(1)时间频度

用T(n)表示

一个算法执行所消耗的时间,理论上不能算出来而是通过运行测试得知,但不可能也没必要对每个算法都做上机测试,只需知道哪个算法花费时间多哪个花费少即可。在算法中一个算法花费的时间和这个算法执行的次数成正比。

在一个算法中,语句执行次数称为时间频度(或称为语句频度),记做为T(n),这里的n代表问题的规模。暂且不考虑这个T是啥,把它理解为一个函数。

(2)时间复杂度

用O(f(n))表示

当n变化时,时间频度T(n)也会不断变化,但是

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值