Java集合
1、hashmap的数组容量一定是2的几次幂
- 在new Hahmap()的时候如果不传递参数,默认的初始化容量就是16
- 如果传递参数,比如传递28,那initcapacity就是28,会走这段代码:(每次左移1位,都乘以2,说明就是2的n次幂)
int capacity=1;
while(capacity<initCapacity)
capacity<<=1;
2、hashmap扩容的边界值是12
- Math.min(capacity装载因子,数组的最大容量+1)求最小值的结果就是160.75=12
- 当数组>12时,数组扩容
3、hashmap存储数据的put方法
- 通过key值获取哈希码h^=k.hascode
- 根据哈希拿到元素在数组的位置h&(table.length-1)相当于h%length
- 添加元素的方法addEntry(当未到达阈值会createEntry,到达阈值会将数组扩容成原来的2倍)
- 当出现哈希碰撞时,7上8下(jdk1.7插入到当前元素之前,当jdk1.8之后会插入到当前元素的后面)(多线程同时操作及逆行扩容时,可能会有循环链表的出现)
- 当出现哈希碰撞时,先比较哈希hash再比较equals(key)值
4、hashmap中计算key的哈希值
jdk1.7
- 通过key获得哈希码的时候,没有直接hascode拿到哈希值
- 而是通过二次散列(通过一个异或的运算h^=k.hascode)
- 核心思想是增加哈希码的不确定性,不会轻易的出现哈希碰撞
h=(h>>>20)(h>>12)等等
jdk1.8时:
4. h=key.hascode()^(h>>>16)也是为了让高16位和低16位同时参与运算,让数据散列更加均匀
5、 扩容的时机
- 第一次put时,数组的长度为null,也会resize扩容
- 数组的长度大于容量*加载因子
- 扩容的大小是左移一位,原来的2倍
6、树化的时机
- 容量大于等于64(当数组的容量没有达到64,会优先选择扩容而不是树化)
- 链表的长度大于等于8
- 为什么大于8的时候会树化?
7、解决哈希冲突的几种办法:
- 再散列法:通过H(key)拿到哈希值出现哈希碰撞后,以这次的H(key)为基础,再次hash,依次类推,直到不出现哈希碰撞为止,再哈希法的哈希表的长度要远远大于需要存放的元素,因为要再哈希,又不能把之前的H(key)删掉,只能做删除标记,不能真正删除结点
- 再哈希法:提供了多种hash方法,第一种hash不行,换第二种,直到找到不出现结点碰撞的哈希方法为止,有点浪费时间
- 拉链法:hashmap就是采用的这种方法,把哈希值相同的放到一个单链表中,把链表的头指针放到对应的哈希表的槽位上
8、树结点:父节点,左节点,右节点,颜色等;链表节点:下一个结点
- 所以树节点的大小是链表节点的2倍左右,一般不会树化
9、JDK1.8和1.7之间的区别
- JDK1.8新增加了红黑树
- JDK1.8从头插变成了尾插
10、hashmap是一个集合容器,用来以键值对的形式进行存储的
- 在JDK1.7和JDK1.8使用的底层数据结构是不一样的:
- JDK1.7使用的是哈希表:数组+链表;JDK1.8使用的是数组+链表+红黑树(红黑树的引入是为了提高查询效率:链表查询的时间复杂度是O(n),红黑树的时间复杂度是O(logn)当链表过长时,会影响hashmap的性能,所以jdk1.8做了优化)
- 当链表到达一定条件时才会树化:一个是当链表的长度大于8时会树化,另一个是当数组容量大于64时,会树化,当没有64时会选择数组扩容
- 再一个是jdk1.7在链表中添加数据时采用的是头插法,jdk1.8换成了尾插法(头插法会有一些问题的产生:循环链表和丢失数据等)
- hashmap的实现原理:在创建hashamap的时候,阿里规约里要求我们传入一个初始化容量,这个初始化容量最好是2的次幂(初始化容量的默认值是16,数据会从1开始左移,左移到最近一个大于到你传入的数值,如果传入28,最近一个2次幂是32)
- 初始化之后使用put添加数据,根据key的哈希值&(数组长度-1)就可以拿到存储的数组下标(用&代替了原来的%运算,效率更高)
- 拿到存储的下标位置后,进行存值,当没有哈希碰撞时,直接addEntry存储进去
- 当出现哈希碰撞时,先比较hash再比较equals(key)值
- 在链表节点存储数据时,JDK1.7使用的是头插法,JDK1.8使用的是尾插法
- 插入数据还会有树化和扩容的问题出现:
- 树化:当数组容量大于64,当链表长度大于8时会树化(树化要将链表左旋右旋,改变颜色,而且树节点的大小差不都是链表结点的两倍,所以一般能不树化就不树化)
- 扩容:当数组的容量大于容量*加载因子时也就是12时会扩容,一般都是扩容到原来的2倍,源码当中好像是左移了1位
- 红黑树的特点:根节点是黑色的,相邻节点的颜色不能相同,从根节点到子节点经过的路径的黑节点的个数必须相同
11、为什么链表的长度大于8时会树化
- 因为在数组的链表上的结点遵循泊松分布的规则
- 当结点出现越多,相同结点出现的概率越小
- 当结点的个数是8时,相同结点出现的概率是0.00006,非常小
- 基本不会到达树化的情况,而且尽量不要树化,因为要把一个链表转为红黑树,要左旋右旋,颜色设置,树化是很消耗性能的
12、头插法会产生的问题
- 当多线程操作时,会形成循环链表,形成死循环,因此JDK1.8使用尾插法插入数据,在扩容的时候保持链表原有的顺序
13、为什么加载因子是0.75
- 是哈希碰撞和空间利用率的一个折中
- 如果加载因子是1,空间利用率很高,但是容易发生碰撞
- 如果加载因子是0.5,虽然不易发生碰撞,但是空间利用率低
14、为什么哈希冲突时,不直接使用红黑树,而是使用链表
- 本来jdk1.8引入红黑树就是为了解决链表过长,造成hashmap性能差
- 本来在链表的长度小于8的时候,能够满足检索效率
- 当链表过长时,红黑树的时间复杂度是O(logn),链表的时间复杂度是O(n),使用红黑树才是更好的选择
- 但是一般不建议树化,而且树化的条件一般也不容易达到:数组容量达到64,链表的长度位8
15、hashmap为什么是线程不安全的
- 在JDK1.7中,插入元素使用的是头插法,但是当多个线程并发操作时,就可能有循环链表的出现,造成死循环,所以在jdk1.8使用的是尾插法
- 多线程操作时,造成元素丢失,当计算出来的索引一样时,那么前一个key就会倍下一个key覆盖掉,造成元素丢失
- 多线程中,get和put同时并发操作时,put的时候需要扩容,那么get的数据就为空
currentHashMap
1、 jdk1.8的初始化容量和hashmap计算方式是不一样的
- hashmap中的初始化容量是距离最近的你传入的那个值,如果传入的是28,那初始化容量就是32
- currenthashmap的容量是传入的初始化容量(init+(init>>>1)+1),然后就是距离最近的一个二次幂值,如果传入的是28,那么初始化容量就是28+14+1=43,也就是距离43最近的一个二次幂也就是64
2、构造方法中有一个sizeCtl变量,当值不同时代表的意义不同
- sizeCtl为0时,表示数组未被初始化,数组的初始化容量就是16
- sizeCtl为正数时,表示被初始化后,扩容的阈值
- sizeCtl为-1时,表示正在初始化
- sizeCtl为-N时,表示正在扩容操作
3、
- currentHashMap不允许空值空键,HashMap允许空值空键
- BST(B+树),AVL(平衡二叉树),RBT(红黑树)
- 调用put方法添加数据时,不允许空值空键,根据key拿到哈希值,并进行一定的扰动,让哈希散列均匀,然后调用init初始化方法,里有两个判断,是否sizeCtl<0,表示正在有线程在做初始化,那就Thread.yield让出CPU的执行权,如果sizeCtl>0表示,就CAS自旋把当前线程的sizeCtl设置成-1,表示当前线程在做初始化,使用DCL(判断了两次table是否为空)的方式new Node,然后将sizeCtl<<2变成原来的0.75倍,siztCtl表示已经初始化完成,当前值是扩容阈值
- 在初始化数组时,并发操作时使用CAS来确保当前时刻只有一个线程做并发的执行,这里用到了一个全局变量sizeCtl将sizeCtl置为-1,表示当前线程正在初始化操作,让别的线程Thread.yeild让出CPU时间片
- 用到了CAS和DCL,当前结点.hascode>0,说明是链表,否则就是数结构
- concurrentHashMap,HashTable
- 转义-中文编码解码,不太好,可以用二进制
4、 concurrent Hash Map和hashmap思路都是一样的都继承了abstractmap,大致的方法都是一样的,红黑树的底层实现不太一样
- concurrentHashMap是如何保证线程安全的?
put方法中:数组init初始化,先通过CAS加设置sizeCtl的值,保证当前只有一个线程会进行数组初始化,其他线程会遇到sizeCtl=-1表示有线程正在初始化,就会Thread.yield让出CPU的执行权,
put方法中:哈希冲突时,使用sych加锁,保证只有一个线程操作
红黑树旋转时:锁住根节点
数组中的槽点拷贝到新数组中时,用转移结点标记原数组的槽点位置,就是不会有线程对当前槽点做数据添加,会等待拷贝结束
5、数组的大小必须是2的n次方
- 如果不是2的n次方,会增加数组下标的碰撞几率,(2^n-1)&哈希值
- 2^n是全一,与哈希值做与运算,会保留哈希值的低位,只要低位不一样,碰撞就不会发生
6、为什么不直接使用hascode,而是使用h^(h>>>>16)&HASH_BITS
- 右移16位,整数型是32位,就是让高位和低位同时参与运算,避免了哈希值不同,但是低16位相同,导致哈希碰撞问题的出现,
- 和HASH_BITS做与运算(0111111…),是为了消除符号位,让哈希值都为正数
7、扩容的思路
- 创建大小为原来两倍的数组容器
- 吧数据从数组的队尾开始拷贝到新数组中去,吧原数组操作设置为转移结点
- 如果有新数据刚好添加时,就会等待,拷贝过程中对应位置上的数据不会发生变化的
8、hashTable和hashMap的异同
- hashmap是线程不安全的,hashtable是线程安全的,基本都被sych修饰
- 因为线程安全,一次只有一个线程去执行,所以效率比较低
- 初始化容量:hashtable初始化容量是11,而hashmap的初始化容量是16
- 扩容大小:hashtable扩容为原来的2n+1倍,而hashmap是原来的两倍
- 底层数据结构:map是链表+红黑,table没有