简单回顾Map相关知识
HashMap
HashMap | HashTable | |
---|---|---|
线程安全性 | 非线程安全 | 线程安全(sychronized关键字保证) |
Null支持 | 支持Null键和Null值 | 不支持 |
扩容机制 | init capacity = 11; 每次扩容为原来的(2n+1) | init capacity = 16; 每次扩容为原来的2n; 如果给了初始容量内部tableSizeFor方法保证其变为大于初始容量的最小2的幂次方 |
HashMap | HashSet | |
---|---|---|
线程安全性 | 非线程安全 | 非线程安全 |
实现接口 | Map接口 | Set接口 |
HashCode计算方式 | 用key计算 | 用对象本身计算,若有哈希碰撞再用equals |
HashMap底层实现
- Jdk1.8之前 数组+链表; Jdk1.8之后 数组+链表+红黑树(提高搜索效率)
- 查找方式:
- 获取key的hashCode值 -> 经过扰动函数(优化哈希值的分布,使其更加均匀) -> 最终的hash值
- 计算(n - 1)& hash 得到最终的元素存储位置
- 判断该位置元素hash值 以及key是否相同,相同覆盖。不同用拉链法解决冲突(其实就是组新节点放链表上了(jdk1.8之后如果链表满足条件(数组length >= 64 && 当前链表长度 > 8(默认阈值))树化了,就放红黑树上了))
- HashMap扩容之后,当前位置的元素新位置要么仍然在原来的位置上,要么在原来的位置+原来的数组长度的新位置上
- 补充 HashMap的长度为什么是2的幂次方:
- hash值在java的范围是 -2147483648 ~ 2147483647,40 亿的映射空间肯定无法用内存存在,所以先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
- 取余(%)操作中如果除数是 2 的幂次则此时 可以等价于 与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1) 的前提是 length 是 2 的 n 次方)。” (位运算的效率极高)
- 保证了上面的第三点,假设原来的长度为16, 新的长度为32。
- 原来的 n - 1 = 16-1 = 15 = 0000 1111
- 新的n -1 = 32-1 = 31 = 0001 1111
- hash = 11101000
- 原来的hash & (n - 1) = 11101000 & 0000 1111 = 1000 index = 8
- 新hash & (n - 1). = 11101000 & 0001 1111 = 1000 index = 8 (不变)(如果原来的hashCode值第五位为1 那么新的位置就是 0001 1000 = 16 + 8 = 24)
HashMap为什么是非线程安全的
- jdk1.8之前的版本,是由于扩容时可能导致死循环和数据丢失(一个线程正在执行 resize() 方法,另一个线程尝试插入新的键值对,就可能会出现死循环)
- jdk1.8解决了死循环,但是还是有可能出现数据覆盖的问题
- 比如两个线程执行put,线程1完成hash判断之后就挂起了,线程二获取时间片执行了同位置的插入,线程1再次拿到时间片之后不会再进行hash判断了。
- 如果一定需要线程安全的Map,可以使用ConcurrentHashMap(不能存储null (key,value都不可以))。
ConcurrentHashMap的线程安全性保证
-
jdk1.7的时候,ConcurrentHashMap = Segment数组 + HashEntry(每个元素是一个链表);采用的是分段锁(Segment)的方式,每个锁锁住一个数组位置。另外Segment 的个数一旦初始化就不能改变。 Segment 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。
-
jdk1.8的时候,ConcurrentHashMap = Node数组 + 链表/红黑树。采用Node + CAS + synchronized 来保证并发安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。也就是JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
- cas 用于添加元素的时候,当桶为空的时候尝试用无锁的cas比较添加元素,当有链表或者红黑树的时候用sychronized锁住执行添加操作。
- 扩容操作需要使用 synchronized 块来确保只有一个线程可以执行扩容操作
- 链表到红黑树转换过程也需要使用 synchronized 块来确保线程安全。
-
另外Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。比如 HashSet,TreeSet,ArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。但是效率很低,尽量不要用,y用JUC包下的集合。
TreeMap
-
TreeMap相较于HashMap多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。(本质是由于TreeMap实现了NavigableMap和SortedMap接口)
-
本人使用这个也比较少,简单列举一下方法(没比较记住,用的时候查即可)并且提供一个代码实例
- firstKey():返回第一个(最小)键。
- lastKey():返回最后一个(最大)键。
- lowerKey(K key):返回小于给定键的最大键。
- higherKey(K key):返回大于给定键的最小键。
- floorKey(K key):返回小于或等于给定键的最大键。
- ceilingKey(K key):返回大于或等于给定键的最小键。
- pollFirstEntry():移除并返回第一个(最小键)的键值对。
- pollLastEntry():移除并返回最后一个(最大键)的键值对。
- headMap(K toKey):返回从第一个键到指定键之前的键值对视图。
- tailMap(K fromKey):返回从指定键开始到最后一个键的键值对视图。
- subMap(K fromKey, K toKey):返回从 fromKey 到 toKey 之前的键值对视图。
-
下面的代码例子,是简单使用了上面的几个方法
import java.util.Map;
import java.util.TreeMap;
public class TreeMapExample {
public static void main(String[] args) {
TreeMap<String, String> capitals = new TreeMap<>();
// 添加一些国家和首都
capitals.put("Italy", "Rome");
capitals.put("France", "Paris");
capitals.put("Germany", "Berlin");
capitals.put("Spain", "Madrid");
// 遍历 TreeMap
// 由于没有指定的比较器,所以默认会用字母排序
for (Map.Entry<String, String> entry : capitals.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
// 使用导航方法
System.out.println("First country: " + capitals.firstKey());
System.out.println("Last country: " + capitals.lastKey());
System.out.println("Capital higher than France: " + capitals.higherEntry("France").getValue());
}
}
- 如果你需要根据自定义的排序规则来排序键,你可以在创建 TreeMap 时传递一个 Comparator
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class CustomTreeMapExample {
public static void main(String[] args) {
// 反转比较
Comparator<String> reverseComparator = (a, b) -> b.compareTo(a);
TreeMap<String, String> capitals = new TreeMap<>(reverseComparator);
// 添加一些国家和首都
capitals.put("Italy", "Rome");
capitals.put("France", "Paris");
capitals.put("Germany", "Berlin");
capitals.put("Spain", "Madrid");
// 遍历 TreeMap
for (Map.Entry<String, String> entry : capitals.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}
}