Java笔记(三):集合
HashMap 和 HashTable 的区别
HashMap | HashTable |
---|---|
线程不安全 | 线程安全 |
继承 AbstractMap | 继承 Dictionary |
允许空的 key 和 value 值 | 不允许空的 key 和 value 值 |
初始长度为16或2的幂次方 | 初始⻓度是11或给定大小 |
每次扩充变为原来的两倍 | 每次扩充容量变为之前的 2n+1 |
entry大于等于8时变为红黑树 | / |
HashMap & HashSet
HashMap
-
HashMap 的长度为什么是 2 的幂次方:
- 对哈希值h进行计算索引的操作等价于
h&(n-1)
,效果和h%n
是一样的,但&操作性能更优。通过将长度设为2的幂次方,可以保证n-1
的二进制表示全为1,这样对哈希值进行位与运算相当于对哈希值的低位取模,从而均匀地将键值对分布到数组的不同位置上。 - 长度为2的幂次方还能够方便地进行数组的扩容和重哈希操作。在扩容时,新数组的长度通常是原长度的两倍,而且新数组的索引位置与旧数组的索引位置之间存在特定的关系,使得元素可以快速地重新映射到新数组中。
- 对哈希值h进行计算索引的操作等价于
-
扩容机制:发生冲突的话则以链表的形式存储,jdk1.8之后引入了红黑树,链表的长度超过8之后会使用红黑树,小于6之后则又转换回来。
-
HashMap 线程安全的实现:ConcurrentHashMap(分段锁,效率最高),HashTable(只有一把锁,期间不能put和get),Collections.synchronizedMap
-
线程不安全原因:在扩容时,新增数据采用头插法,会造成链表翻转形成闭环,jdk1.8之后就不再采用头插法了,而是直接插入链表尾部,因此不会形成环形链表形成死循环,但是在多线程的情况下仍然是不安全的,在put数据时如果出现两个线程同时操作,可能会发生数据覆盖,引发线程不安全,总之,用ConcurrentHashMap没错了。
HashSet
- HashSet 继承于 AbstractSet 接口,实现了 Set、 Cloneable,、 java.io.Serializable 接口。 HashSet 不允许集合中出现重复的值。HashSet 底层其实就是 HashMap,所有对 HashSet 的操作其实就是对HashMap 的操作。所以 HashSet 也不保证集合的顺序,也不是线程安全的容器。
ConcurrentHashMap
JDK 1.7: ReentrantLock+Segment+HashEntry
HashEntry数组 + Segment数组 + Unsafe 「大量方法运用」
JDK1.7中数据结构是由一个Segment数组和多个HashEntry数组组成的,每一个Segment元素中存储的是HashEntry数组+链表,而且每个Segment均继承自可重入锁ReentrantLock,也就带有了锁的功能,当线程执行put的时候,只锁住对应的那个Segment 对象,对其他的 Segment 的 get put 互不干扰,这样子就提升了效率,做到了线程安全。
额外补充:我们对 ConcurrentHashMap 最关心的地方莫过于如何解决 HashMap 在 put 时候扩容引起的不安全问题?
在 JDK1.7 中 ConcurrentHashMap 在 put 方法中进行了两次 hash 计算去定位数据的存储位置,尽可能的减小哈希冲突的可能行,然后再根据 hash 值以 Unsafe 调用方式,直接获取相应的 Segment,最终将数据添加到容器中是由 segment对象的 put 方法来完成。由于 Segment 对象本身就是一把锁,所以在新增数据的时候,相应的 Segment对象块是被锁住的,其他线程并不能操作这个 Segment 对象,这样就保证了数据的安全性,在扩容时也是这样的,在 JDK1.7 中的 ConcurrentHashMap扩容只是针对 Segment 对象中的 HashEntry 数组进行扩容,还是因为 Segment 对象是一把锁,所以在 rehash 的过程中,其他线程无法对 segment 的 hash 表做操作,这就解决了 HashMap 中 put 数据引起的闭环问题。
JDK 1.8: Synchronized+CAS+Node+红黑树
JDK1.8屏蔽了JDK1.7中的Segment概念呢,而是直接使用「Node数组+链表+红黑树」的数据结构来实现,并发控制采用 「Synchronized + CAS机制」来确保安全性,为了兼容旧版本保留了Segment的定义,虽然没有任何结构上的作用。
总之JDK1.8中优化了两个部分:
- 放弃了 HashEntry 结构而是采用了跟 HashMap 结构非常相似的 Node数组 + 链表(链表长度大于8时转成红黑树)的形式
- Synchronize替代了ReentrantLock,我们一直固有的思想可能觉得,Synchronize是重量级锁,效率比较低,但为什么要替换掉ReentrantLock呢?
- 随着JDK版本的迭代,本着对Synchronize不放弃的态度,内置的Synchronize变的越来越“轻”了,某些场合比使用API更加灵活。
- 加锁力度的不同,在JDK1.7中加锁的力度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点),也就是1.8中加锁力度更低了,在粗粒度加锁中 ReentrantLock 可能通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了,所以使用内置的 Synchronize 并不比ReentrantLock效果差。
Vector 和 ArrayList 的区别
ArrayList | Vector |
---|---|
线程不安全 | 线程安全 |
1.5倍扩容 | 2x扩容 |
/ | 链表实现 |
线程安全的集合
var list = new Vector<>(); // deprecated 使用synchronized修饰add
var list = Collections.synchronizedList(new ArrayList<>());
var list = new CopyOnWriteArrayList<>(); // 写入时复制 使用lock在add中,效率较高
Arrays.asList 获得的 List 应该注意什么
-
Arrays.asList 转换完成后的 List 不能再进⾏结构化的修改,什么是结构化的修改?就是不能再进
⾏任何 List 元素的增加或者减少的操作。 -
Arrays.asList 不⽀持基础类型的转换