Java集合_Map
Map
Map常用实现类
类 | 并发性 | 有序性 | 底层数据结构 | 初始容量 | 负载因子 | 实例化方式 | 一致性 | k/v是否可为null |
---|---|---|---|---|---|---|---|---|
HashMap | 不支持 | 无序 | 数组+链表/红黑树 | 16 | 0.75 | 懒加载(第一次put元素才会会初始化容量) | - | k/v可为null |
LinkedHashMap | 不支持 | 有序(插入序或者访问序) | 数组+单向链表+双向链表 | - | - | - | - | k/v可为null |
TreeMap | 不支持 | 自然序(左小右大) | 红黑树 | - | - | - | - | 仅v能为null |
ThreadLocalMap | 不支持 | 无序 | 数组 | 16 | 0.75 | 懒加载 | - | 仅v能为null |
HashTable | 支持 | 无序 | 数组加链表 | 11 | 0.75 | 初始化创建 | 强一致性 | 均不能为null |
ConcurrentHashMap(1.7) | 支持 | 无序 | 分段锁+数组+链表 | 16 | 0.75 | 懒加载 | 强一致性 | 均不能为null |
ConcurrentHashMap(1.8) | 支持 | 无序 | 数组+链表/红黑树+CAS结构 | 16 | 0.75 | 懒加载 | 弱一致性 | 均不能为null |
ConcurrentSkipListMap | 支持 | 自然序(左小右大) | 跳跃表 | - | - | - | 弱一致性 | 均不能为null |
Map是一个将Key映射到Value的对象。
一个Map不能包含重复的Key,每个Key最多只能映射一个value。
Map中的元素,无序、键不重,值可重、可一个空键、多个空值;注意,这里的无序是指是否按照插入的顺序输出元素,而不是输出元素是否排序,实现类TreeMap的元素输出是有序的,TreeMap按照key的字典顺序来排序(升序),不是按照数字顺序,而是字典顺序
- 举个例子:treeMap中put三个String类型的key和Integer类型的value:
treeMap.put("9", 67); treeMap.put("10", 2); treeMap.put("11", 67);
输出的顺序是:
10 11 9
因为String类型的key比较数字时,首先比较首字母,首字母相同比较后一位,1比9小,所以10和11排前面,0比1小,所以10排在11前面
Map/Set如何保证Key唯一(Map和Set不可存储重复元素)
Map和Set中不同的实现类,Hash类和Tree类其确保Key唯一的方法不同。
HashMap、HashSet:底层数据结构都是HashTable(1.8+红黑树),容器中的元素全部存储在HashTable中;每一个被添加的元素都会有一个hashCode(哈希值),JVM会将该元素与表中元素对比是否有相同的哈希值,不相同则进入HashTable;如果hashCode相同的话,再去比较equals()方法,相同则被认为数据已存在,无法添加进入hashTable中,JVM自动生成链表存储,并且在上一次计算的hashCode再次计算hashCode(reHash)
TreeMap TreeSet:底层数据结构为二叉树,容器中添加元素时会调用Conparable中的conpareTo()方法,返回-1,添加到左子树;返回+1,添加到右子树;返回0,表示元素重复不添加
Collections.synchronizedMap和ConcurrentHashMap有什么区别
SynchronizedMap:重量级锁,通过synchronized关键字来保证操作的线程安全,一次锁住整张表来保证线程安全,该集合所有的数据都会被上锁,所以每次只能有一个线程来访为 map,类似的还有Collections.synchronizedList等集合(锁住整个对象)。
-
并发情况下修改数据可能会报出ConcurrentModificationException异常,并发修改快速失败;
-
传入的是序列化数据返回的也会是序列化的
- 典型的装饰器模式(包装Wrapper模式):通过装饰器模式来提供一个装饰器类(SynchronizedMap)对原始类(HashMap)进行包裹,并且装饰器类(SynchronizedMap)与原始类实现相同的接口(Map),装饰器类(SynchronizedMap)对Map接口的实现委托给了原始类(HashMap)来实现,而装饰器类(SynchronizedMap)则在原始类(HashMap)的基础上,对原始类(HashMap)的功能实现了增强,对应到SynchronizedMap实现中,提供的增强功能就是在HashMap的基础上增强了线程安全的保障。
ConcurrentHashMap:使用分段锁,将我们的所有数据一段一段上锁,在你操作这段数据的时候,另一个线程依然可以操作其他段的数据,使用分段锁来保证在多线程下的性能(锁住对象的部分内存)。
ConcurrentHashMap 使用了cas+synchronized解决共享遍历操作原子性问题,使用volatile保障共享变量的内存可见性问题。是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶,诸如 get,put,remove 等常用操作只锁当前需要用到的桶。 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提 升是显而易见的。
另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中,当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而 不影响原有的数据 ,iterator 完成后再将头指针替换。
为新的数据 ,这样 iterator 线程可以使用原来老的数据,而写线程也可以并发的完成改变。
HashMap
HashMap是非线程安全的,即不支持并发操作;其次它是无序的,因为存储位置跟哈希值计算相关,因此是无序的;底层数据结构在jdk1.7和jdk1.8是不同的,1.7版本的hashMap的底层数据结构是数组+链表;为解决哈希碰撞,在jdk1.8引入了红黑树,红黑树本质是一种平衡二叉树,通过旋转和着色来使树平衡;hashMap的初始容量在jdk1.8时默认是0,由于hashMap实例化方式是懒加载模式,只有在第一次put操作时才会进行初始化容量,初始扩容容量为16,这是调用无参构造或没有指定初始大小的构造函数实例化对象时的场景;
HashMap主要有由数组table和链表/红黑树组成,当链表的长度为8的时候开始准备转为红黑树,当桶的个数大于64时,才会正式转为红黑树,否则只会扩容,而不会树化;当红黑树的长度小于等于6则转化为链表。
HashMap树化条件
-
链表长度超过阈值:默认情况下,当链表长度超过 8 时,HashMap 会将链表转化为红黑树。这是因为链表的查找和插入操作的时间复杂度为 O(n),而红黑树的时间复杂度为 O(log n),所以当链表长度过长时,使用红黑树能够提高性能。
-
HashMap 的容量超过阈值:当 HashMap 的容量超过 64 时,并且链表长度超过 8,才会进行树化操作。这是因为只有在容量比较大的情况下,树化操作才能够真正提高性能,否则转化为红黑树的开销可能会超过链表操作的开销。
HashMap转红黑树:容量大于等于64且链表长度为8才会进行树化,否则只会进行扩容
HashMap树退链表条件:
-
链表长度小于等于阈值:当红黑树节点数小于等于 6 时,HashMap 会将红黑树取消树化,重新转化为链表。这是因为当链表长度较短时,使用链表进行查找和插入操作的性能更好,不需要额外的红黑树的复杂性和开销。
-
HashMap 的容量小于等于阈值:当 HashMap 的容量小于等于 64 时,并且红黑树节点数小于等于 6,会将红黑树取消树化。这是因为当容量较小时,使用链表进行操作的性能更好,而树化操作可能会带来额外的内存开销。
如果因为删除数据或扩容导致红黑树的元素小于6,红黑树会变回链表
扩容阈值=size*LOAD_FACTOR
一般情况下,size默认是16,负载因子LOAD_FACTOR 默认为0.75
树化和扩容都比较耗性能。所以在阿里规约中要求我们初始化容量的根本目的就是文为了让我们减少扩容的次数。
阿里规范中给了一个初始化容量的计算的方式:
这个数据即使不是2的幂次方,在实例化时后也会转化为2的幂次方,比如要存入数据数量为90个,那么计算后的初始容量就是(90 / 0.75)+1 = 121,在实例化时会实例出一个容量为 128 的 hashMap 。
HashMap 存储键值流程图
putVal源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 如果table没有初始化,或者初始化的大小为0,进行resize操作 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果hash值对应的桶内没有数据,直接生成结点并且把结点放入桶中 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 如果hash值对应的桶内有数据解决冲突,再放入桶中 else { Node<K,V> e; K k; //判断put的元素和已经存在的元素是相同(hash一致,并且equals返回true) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // put的元素和已经存在的元素是不相同(hash不一致,或equals返回false) // 如果桶内元素的类型是TreeNode,也就是解决hash解决冲突用的树型结构,把元素放入树种 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 桶内元素的类型不是TreeNode,而是链表时,把数据放入链表的最后一个元素上 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 如果链表的长度大于转换为树的阈值(TREEIFY_THRESHOLD),将存储元素的数据结构变更为树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st TREEIFY_THRESHOLD - 1=7 treeifyBin(tab, hash); break; } // 如果查已经存在key,停止遍历 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 已经存在元素时 //查询是否存在给定key的映射值; //在HashMap中更新给定key的映射值,并返回旧的映射值。如果不存在该key的映射值,则返回null。 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 如果K-V数量大于阈值,进行resize操作 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
ConcurrentSkipListMap
ConcurrentSkipListMap
是 Java 中的一个线程安全的有序映射(Map)实现。它是基于跳表(Skip List)数据结构实现的,可以提供高效的插入、删除和查找操作。
与其他线程安全的 Map 实现相比,ConcurrentSkipListMap
的特点是支持并发访问,多个线程可以同时对其进行操作而不会导致数据的不一致性。它采用了乐观并发控制的方式来实现高并发性能,通过使用 CAS(Compare and Swap)操作来确保对数据的原子性操作。
ConcurrentSkipListMap
中的元素是有序的,它根据键的自然顺序或者根据自定义的比较器进行排序。这使得它在需要按顺序访问元素的场景中非常有用。
ConcurrentSkipListMap具有以下特点:
-
有序性:ConcurrentSkipListMap中的键值对是按照键的自然顺序进行排序的,或者可以通过传入的Comparator进行自定义排序。因此,可以在ConcurrentSkipListMap中以有序的方式遍历键值对。
-
并发性:ConcurrentSkipListMap可以支持多线程环境下的并发访问和修改操作。它使用了一种基于层级的锁策略,使得不同的线程可以同时访问不同的层级,从而提高了并发性能。
-
高效性:ConcurrentSkipListMap的插入、删除和查找等操作具有较高的效率。在包含大量元素的情况下,ConcurrentSkipListMap的性能与并发性能可以与其他并发映射实现相媲美。
ConcurrentSkipListMap的插入、读取数据时复杂度是 O(logn);
注意,ConcurrentSkipListMap相对于其他并发映射实现(如ConcurrentHashMap)来说,更适用于有序操作和范围查询的场景。如果不需要有序性或者只关注键值对的存取性能,可能会选择其他的并发映射实现。