ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java集合扩容机制笔记

2022-06-14 17:34:01  阅读:172  来源: 互联网

标签:扩容 Java 容量 00000000 笔记 线程 数组 集合 hash


一、ArrayList

1、ArrayList构造函数

 

 

 

1.1如果指定了容量大小,创建该大小的数组

1.2如果没有指定大小,默认创建空数组

1.3如果是指定小于0的大小,抛出异常

无参构造:创建空数组,在添加第一个元素时候才会扩容到10的容量。

 

 

 !只有在jdk6中会一开始就创建一个数组大小为10的数组。

2、添加元素是添加在数组末尾。(先确保数组容量)

 

 

 3、ensureExplicitCapacity判断是否需要扩容。

 

4、grow()方法

得到旧容量,将旧容量扩大1.5倍(大约)。

比如说原始大小是oldCapacity=7,7+7>>1=7+3=10.  

 

 

 如果扩容后的容量还不够,就用需要的容量当做最新的容量。

补充:

数组是length。

字符串是length()。

泛型集合是size()。

 

二、HashMap

主要存放键值对,是非线程安全的

key和value都可以存储空值,只能存一个空值key和多个空值value。

HashmapJDK1.8以前是数组+链表结合使用。1.8会在扩容机制变化后,演变为红黑树。

1、JDK1.8hashMap的hash方法源码:

JDK7中的hash算法全是取余。

 

JDK取得了hashcode,还会右移,为了加入扰动,降低hash冲突

2、loadFactory加载因子是控制数组存放疏密程度。

越趋近于1,越容易冲突。

给定的负载因子是0.75是官方给顶的。

初始容量是16,当加入到12时,会进行扩容。

扩容会涉及到rehash、复制数据等操作。

(这个地方发现put中调用的putValue方法,这个是default权限的方法,访问权限是本包类中,除开本包类就变成了private权限)

Hashmap默认是没有大小,只会第一次使用put->putVal中的resize才会初始化。一开始new HashMap();

putVal()方法流程:

 

 

 

 1、判断当前table是否为空,如果为空,调用resize()进行扩容。返回table的长度。

 2、根据(n-1)&hash来求出在数组中的位置,判断当前是否发生冲突

  2.1如果没有冲突直接插入新值

  2.2如果冲突 ,判断当前key是否相同,相同直接覆盖当前的key和value值

   如果当前是红黑树,插入到红黑树。

     判断当前长度是否大于8,如果大于8,看当前数组是否是>=64,是就将当前链表转为红黑树。

   否则只是扩容hashmap的数组。扩容完以后,再遍历插入。

问题:为什么hashmap长度是2的幂次方

Hash值范围Integer.Max -2147483648到2147483647但是内存装不下。

数组下标的计算方法是当前hash&(n-1)。 hash计算是

 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

优点:
1、(n-1)的目的是因为2的n次方是 1000....0000,这种二进制数,减掉1过后是1111...1111这样可以降低hash冲突,并且降低空间浪费。
如果是其他数可能会造成某些数组空间永远存不上值。

例子5的二进制   00000000 00000000 00000000 00000101
发现任何一个数&上5,倒数第二低位永远是0,

   2、在求数组下标的时候,本身就应该对数组长度求余,但是,取余运算会更快,key.hashcode%arr.length==key.hashcode&(arr.length-1) 只有当arr.length是2的次幂才会相等。

   3、hashmap 扩容rehash过后,元素新的位置,要么在原角标位置,要么在原角标+扩容位置上。

比如扩容前长度是8,扩容后长度是16

第一种情况:
扩容前:
00000000 00000000 00000000 00000101
&00000000 00000000 00000000 00000111 8-1=7
-------------------------------------
101 ===== 5 原来脚标位是5

扩容后:
00000000 00000000 00000000 00000101
&00000000 00000000 00000000 00001111 16-1=15
-------------------------------------
101 ===== 5 扩容后脚标位是5(原脚标位)


第二种情况:
扩容前:
00000000 00000000 00000000 00001101
&00000000 00000000 00000000 00000111 8-1=7
-------------------------------------
101 ===== 5 原来脚标位是5

扩容后:
00000000 00000000 00000000 00001101
&00000000 00000000 00000000 00001111 16-1=15
-------------------------------------
1101 ===== 13 扩容后脚标位是13(原脚标位+扩容长度)

 

 

 

 

2、resize()方法:

1、如果当前oldTable容量是否为空,如果为空,指定为0。

  1.1 老容量大于0,如果大于指定的最大值Integer.MaxVALUE就不会扩容了。

  1.2 如果没有超过最大值,就扩充为原来的两倍。并且新阈值也要扩充为两倍。

2、如果当前为空,并且老阈值不为0,新的容量等于老的阈值。

3、新的容量为16,阈值=默认为16*负载因子0.75

4、再进行rehash

 

 

 

JDK7 JDK8  HashMap线程不安全的原因:

JDK7:由于多线程对HashMap扩容,resize方法的transfer方法中,采用的头插法。

某个线程执行中,挂起,其他线程完成了数据迁移,等CPU释放资源后被挂起的线程重新执行逻辑,会造成链表的死循环。

先put插入元素,调用addEntry方法,判断是否超过阈值,超过就会调用resize,resize再调用transfer

在最后三行,造成循环链表。

rehash过程,先扩容2倍的新空间

再头插法,移动元素。

真实案例:

 

 两个线程目前都要扩容,线程A执行e1指针指向3,线程B执行e2指向3,并且线程A,B的next1,、next2都指向2。

假如这个时候线程B时间片消耗完了,if(hash)代码后挂起,线程A目前扩容已经完成了:

 

并且这个时候线程B也是有个新数组的,长度同样是之前的2倍。

 

 

 

 

new table[i]=e,e称为了第一个节点,e指针指向了next所以 

左边next变成了e。 

下次遍历:

e.next取到3这个节点

 

 

 

 

 

e.next=newtable[i] 就出现环状。

 

 

 

 

 

 

 

 

JDK8:由于多线程对HashMap进行put操作,调用了putval,具体原因是假设两个线程A,B都put,并且hash函数插入的下标相同,当线程A执行完后,时间片消耗完,挂起。这个时候B线程获得时间片后

在该下标处插入了元素,完成了正常的插入,而A获得时间片之前已经检查过了是否冲突,所以不会检查,直接导致线程B插入的数据被覆盖。线程不安全。

 

标签:扩容,Java,容量,00000000,笔记,线程,数组,集合,hash
来源: https://www.cnblogs.com/Alei777/p/16375565.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有