头插法和尾插法
在JDK1.7的时候是头插法,而JDK1.8及其以后使用了尾插法:当多线程同时操作的时候,可能会导致死循环。
头插法会导致链表翻转。
JDK1.8:在多线程情况下也有存在数据丢失的情况,但是不会导致死循环。
在JDK1.7的时候,使用了entry来存放数据,然后在1.8以后使用了Node
原因(个人猜测):为了处理哈希碰撞,实现了链表转红黑树,而红黑树有TreeNode和LinkNode两种方式,所以就转化为Node。源码上来看也是Entry的二次封装。
为什么负载因子是0.75
因为随机分布的话,0.75左右数据会很少。
哈希表为什么线程不安全
由于为了提高读写性能,使用了缓存——而不同的线程的缓存对于其他线程是不可见的。
其他线程读取的数据很有可能是已经修改或者作废的老数据。
JDK1.7会出现死循环、数据丢失和数据覆盖的问题,但是JDK1.8还是有数据覆盖的问题。
拉链法
就是使用数组 + 链表的方式来存储数据。也就是哈希表的方式
开放定址 /散列法:先去进行哈希,当出现哈希碰撞的时候进行移位直到找到合适的位置。
cas操作和分段锁
在JDK1.7的时候,使用了分段锁Segment(继承于ReentrantLock)实现多线程并发的CurrentHashMap。
但是在JDK1.8的时候使用了cas操作 + synchronized实现的。
cas操作底层使用了unSafe这个类。会同时记录旧值和新值。当旧值被修改的时候会发生自旋。
-
都是多把锁,每个锁之间是可以并行的,但是单个锁是串行的。
-
分段锁最多支持16把锁,所以并发不如cas。
加锁的时候加入了写锁,但是读不加锁。
可并行的线程和哈希表的分段大小相关。
hashTable所有操作都是加锁的,所以并发不行。
hashMap有线程不安全的问题。
哈希表扩容
双倍扩容。当数据的数量大于数据量 * 负载因子的时候会触发扩容的操作。
当单链的长度大于8的时候会转化为红黑树,小于6则退化为链表。原因是因为会发生哈希碰撞导致时间复杂度退化到N ^ 2
哈希表的默认大小必须是16的整数倍(最小是16)且大小是2 * N次方,以便取模进行位运算。
迭代器方法
Java的for each实现的时候会使用迭代器。而迭代器会比较modCount记录的修改次数,如果ModCount增加的时候会抛出异常。
而普通的for方法不比较modCount,所以会导致修改。
但是:当修改hash表中的数据的时候,modCount不会变化。只有在移除和增加节点的时候modCount才会发生变化。
哈希值的计算方式
是通过高16位和低16位进行异或操作,让数据尽可能的分散。(遍历链表的时间复杂度为N,而遍历数组的时间复杂度是1)
哈希表的时间复杂度
哈希表的时间复杂度均摊为1。
哈希表的设计保证了:基本上不会发生哈希碰撞,链表的时间复杂度是N,哈希表的时间复杂度是LogN这些懂事小概率事件。
所以可以表示为数据的时间复杂度为1
TreeMap的底层是通过红黑树来实现,他的平均时间复杂度是OLogN。
为什么使用红黑树:因为会进行左旋右旋以保证二叉树的深度是LogN,而不会出现深度过高,保证查询的时间复杂度是LogN。
但是左旋和右旋的操作也比较消耗性能。所以相对传统的AVL树,红黑树保证每次插入不超过3次修改(我到现在还没有实现过红黑树……)
哈希表有序
哈希表因为会出现哈希,所以读取的时候会乱序。
如果要有序的话需要使用LinkedHashMap来实现。
重写hashCode方法
哈希表比较的时候如果是自定义的类需要重写hashCode方法和equals方法
原因:
- 不重写的话是直接比较内存地址
- 而比较之前需要比较哈希值然后比较数据
null值
HashMap支持null,但是hashTable不支持。
原因:null没有hashCode方法,会抛出空指针异常。而hashMap加入了判断
key = null return 0
CurrentHashMap不支持Null值。因为:
- 可能会出现歧义,是不存在还是null,为了防止这个问题所以直接不用