Bota5ky

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的幂次方还能够方便地进行数组的扩容和重哈希操作。在扩容时,新数组的长度通常是原长度的两倍,而且新数组的索引位置与旧数组的索引位置之间存在特定的关系,使得元素可以快速地重新映射到新数组中。
  • 扩容机制:发生冲突的话则以链表的形式存储,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 不⽀持基础类型的转换

posted @ 2023-03-18 22:59  Bota5ky  阅读(4)  评论(0编辑  收藏  举报