4. Map

1. HashMap(数组+链表+红黑树)
HashMap根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。
HashMap最多只允许一条记录的键为null,允许多条记录的值为null。
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据不一致。如果需要满足线程安全,可以用Collections的SynchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。

1)JAVA 1.7实现

底层数据结构为数组+链表,大方向上,HashMap里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类Entry的实例,Entry包含四个属性:key、value、hash值和单向链表的next。
a)capacity:当前数组容量,默认为16,始终保持2^n,可以扩容,扩容后的数组大小为当前的2倍。
b)loadFactor:负载因子,默认为0.75。
c)threshold:扩容的阈值,等于capacity*loadFactor
d)HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容条件就是当HashMap中的元素个数超过临界值时就会自动扩容(threshold = loadFactor * capacity)。如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容。而HashMap每次扩容都需要重建hash表,非常影响性能。所以建议开发者在创建HashMap的时候指定初始化容量。

2)JAVA 1.8实现

底层数据结构为数据+链表+红黑树,1.8对HashMap做了一些修改,最大的不同就是利用了红黑树
根据1.7的介绍,我们知道,查询的时候,根据hash值我们能快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为O(n)。为了降低这部分的开销,在1.8中,当链表的元素超过了8个以后,会将链表转为红黑树,在这些位置进行查找的时候可以降低时间复杂度为O(logN)。

3)红黑树的特征

4)HashMap put方法的流程

a)如果table没有初始化就先进行初始化过程
b)使用hash算法计算key的索引
c)判断索引处有没有存在元素,没有就直接插入
d)如果索引处存在元素,则遍历插入,有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
e)链表的数量大于阈值8,就要转换成红黑树的结构
f)添加成功后会检查是否需要扩容

5)HashMap扩容过程
a)1.8扩容机制:当元素个数大于threshold时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。
b)1.7链表新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,1.8采用了尾插法,避免了这种情况的发生。
c)原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据e.hash & oldCap == 0判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。

2. ConcurrentHashMap
多线程环境下,使用Hashmap进行put操作会引起死循环,应该使用支持多线程的 ConcurrentHashMap。
ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry,Segment 数组大小默认是 16,2 的 n 次方;
JDK 1.8 之后取消了segment分段锁,采用 Node + CAS + Synchronized来保证并发安全进行实现。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,相比1.7锁定HashEntry数组,锁粒度更小,支持更高的并发量。当链表长度过长时,Node会转换成TreeNode,提高查找速度。

1)JAVA 1.7实现

a)Segment段
ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一
些。整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的
意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个
segment。
b)线程安全(Segment继承ReentrantLock锁)
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承
ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每
个 Segment 是线程安全的,也就实现了全局的线程安全。
c)并行度(默认 16)
concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,
也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支
持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时
候设置为其他值,但是一旦初始化以后,它是不可以扩容的。再具体到每个 Segment 内部,其实
每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

2)JAVA 1.8实现
Java8 对 ConcurrentHashMap 进行了比较大的改动,Java8 也引入了红黑树。

3)ConcurrentHashMap put方法执行流程
在put的时候需要锁住Segment,保证并发安全。调用get的时候不加锁,因为node数组成员val和指针next是用volatile修饰的,更改后的值会立刻刷新到主存中,保证了可见性,node数组table也用volatile修饰,保证在运行过程对其他线程具有可见性。

点击查看代码
transient volatile Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
    volatile V val;
    volatile Node<K,V> next;
}
**put方法操作流程:** a)如果table没有初始化就先进行初始化过程 b)使用hash算法计算key的位置 c)如果这个位置为空则直接CAS插入,如果不为空的话,则取出这个节点 d)如果取出来节点的hash值是MOVED(-1)的话,则表示当前正在对这个节点进行扩容,复制到新的数组,则当前线程也去帮助复制 e)如果这个节点不为空也没有在扩容,则通过synchronized来加锁,进行添加操作,这里有两种情况,一种是链表形式就直接遍历到尾端插入或者覆盖掉相同的key,一种是红黑树就按照红黑树结构插入 f)链表的数量大于阈值8,就会转成红黑树的结构或者进行扩容(table长度小于64) g)添加成功后会检查是否需要扩容

4)如何进行扩容
数组扩容transfer方法中会设置一个步长,表示一个线程处理的数组长度,最小值是16。在一个步长范围内只有一个线程会对其进行复制移动操作。

3. HashTable(线程安全)
1)HashTable是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自Dictionary 类
2)HashTable中大部分public修饰普通方法都是 synchronized 字段修饰的,是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap。
3)HashTable 的 key 不能为 null,value 也不能为 null,会抛出空指针异常
4)HashTable 直接使用对象的 hash 值。hash 值是 JDK 根据对象的地址或者字符串或者数字算出来的 int 类型的数值。然后再使用除留余数法来获得最终的位置。然而除法运算是非常耗费时
间的,效率很低。
5)默认情况下,HashTable的初始长度是 11,之后每次扩充容量变为之前的2n+1(n 为上一次的长度)
6)HashTable 是线程安全,推荐使用 HashMap 代替 HashTable;如果需要线程安全高并发的话,推荐使用 ConcurrentHashMap 代替 HashTable。

4. TreeMap(可排序)
TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。

点击查看代码
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
}
**TreeMap 的继承结构:** ![](https://img2024.cnblogs.com/blog/3579780/202412/3579780-20241223100530770-1055567967.png) **TreeMap的特点:** TreeMap是有序的key-value集合,通过红黑树实现。根据键的自然顺序进行排序或根据提供的Comparator进行排序。 TreeMap继承了AbstractMap,实现了NavigableMap接口,支持一系列的导航方法,给定具体搜索目标,可以返回最接近的匹配项。 如floorEntry()、ceilingEntry()分别返回小于等于、大于等于给定键关联的Map.Entry()对象,不存在则返回null。 lowerKey()、floorKey、ceilingKey、higherKey()只返回关联的key。

5. LinkedHashMap(记录插入顺序)
LinkedHashMap 是 HashMap 的一个子类,保存了记录的插入顺序,在用 Iterator 遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

HashMap是无序的,迭代HashMap所得到元素的顺序并不是它们最初放到HashMap的顺序,即不能保持它们的插入顺序。
LinkedHashMap继承于HashMap,是HashMap和LinkedList的融合体,具备两者的特性。每次put操作都会将entry插入到双向链表的尾部。

posted on 2024-12-23 10:20  南柯易梦  阅读(33)  评论(0)    收藏  举报