fail-fast(快速失败)和fail-safe(安全失败)是两种在遍历集合时处理并发修改的策略。
遍历集合时,如果发现集合被修改(除了通过迭代器自身的remove方法),会立即抛出ConcurrentModificationException异常。这种机制的目的是快速检测到不一致性并报告错误。如,使用ArrayList时,若一线程中正在遍历,而另一线程对其进行了修改,就可能触发fail-fast。HashMap、HashSet也有这个机制。
遍历集合时,会先复制一份集合,然后在复制的集合上遍历。这样,即使原始集合在遍历过程中被修改,也不会影响正在进行的遍历,不会抛异常。CopyOnWriteArrayList就是采用了fail-safe机制。
在多线程环境中,多个线程可能同时操作一个集合。
总的来说:
数组和链表的读快写快区别就不说了。
数组+链表+红黑树,通过散列映射来存储键值对数据。
HashMap的key、value都可以为null。
放到hashMap里的key,已经有hashCode了,为啥还要被hashMap自己的hash方法再hash一把呢?
答:是为了让高位和低位都参与hash,把hashCode匀一下,哈得更稀一些。
为啥高位和低位都一起hash一下,可以和得更均匀呢?
举个例子:
想象一个拥有3个货架(编号1至3)的菜鸟驿站,它坐落在3个小区(分别叫a、b、c小区)之间,小区的居民平时会寄卡片、买衣服、买书本。
我们做一下类比:
- 用货架类比hashMap里的数组,3个货架就类比成一个大小是3的数组;
- 用小区编号类比key的高位、用居民买的东西类比key的低位。
- 我们不知道哪些个小区买哪些类东西多,但我们希望尽量让每个货架堆放东西的数量比较平均,该怎么做呢?
我们有以下3个方案:
- 一个小区一个货架(只用高位来判断存储)
- 一个品类一个货架(只用低位来判断存储)
- 交叉放,但要有个关系映射,比如:1货架放a小区的卡片、b小区的衣服、c小区的书本;2货架放b小区的卡片、c小区的衣服、d小区的书本;3货架放c小区的卡片、a小区的一方、b小区的书本
对方案1来说,如果某个小区很少买东西,它的货架基本就空的;对方案2来说,如果大家很少买某个品类的东西,这个品类对应的货架也是空的;对方案3来说,以上两个问题都不存在,这就是高低位都混到一起搅一下后的好处。
其中hash位与0x7FFFFFFF只是为了让hash变成正数
数组大小-1后,一定变成全低位全是1的二进制形式,如00001111、00111111、00000011这样。为啥一定会变成这样?因为hashMap的数组大小始终是2的n次幂形式(可以看resize的代码,确保了这一点,即使初始化hashMap为非2次幂的形式,也会被它强制干成2次幂形式),所以数组大小-1后,就成了低位全是1了。
比如数组大小是16(2的4次方,二进制形式是00010000),它减去1后就变成了15(二进制是00001111)。
位与的性能比取模更高,而且一般来说,二进制位上的位与,其随机性比取模的要搞,所以位置冲突会少一些。
默认的负载因子是0.75,如果数组中已经存储的元素个数大于数组长度的75%,将会引发扩容操作。
简单来说:
找到位置,太长就转红黑树,插入,插完了看是不是太长,太长就扩容。
结构上跟hashmap比较像,jdk1.7用了分段锁,一个段管几个table的位置,有多个段。
synchronized在Java后续的版本中进行了大量的性能优化,其性能已经不逊于甚至优于ReentrantLock。
jdk对synchronized做的优化:
在锁住要插入位置的桶后,操作与HashMap比较相似:都会进行查找是否存在相同键、更新值或插入新节点等基本操作。
但也存在一些不同:ConcurrentHashMap是在多线程环境下操作的,需要时刻注意线程安全,在操作过程中可能会有其他线程同时访问其他桶。在处理冲突节点(如链表或红黑树)的插入、更新等操作时ConcurrentHashMap会有一些同步机制。
在JDK1.8中,ConcurrentHashMap的put操作大致如下:
在JDK1.8中,ConcurrentHashMap的get操作无需加锁,操作与HashMap比较相似,大致流程如下:
由于get操作不涉及对数据结构的修改,只是读取操作,所以不需要加锁,而且Node的元素value和指针next都是用volatile修饰的,被其他线程修改是可见的。
table这个数组也用volatile修饰了,目的是保证在数组扩容的时候多线程之间的可见性。
key、value不能为null的原因类似:在get和contains之间,其他线程可能会过来捣乱,使得我们不知道到底是有个值是null,还是根本没有这个值。
下面用value不能为null详细说明:
如果value可以为null,那get(key)得到null的时候,就不知道(1)是这个key对应的value是null,(2)还是这个key都不存在。在HashMap里,这种情况下为了区分是(1)还是(2),可以用containsKey(key)来判断一下,如果containsKey(key)是true,说明不是(2),否则就是(2)。但在ConcurrentHashMap中,如果在调用containsKey(key)判断之前被别的线程塞了个(key, null)键值对进去,就会得到结果是(1);如果在调用containsKey(key)判断之前“没有”被别的线程塞了个(key, null)键值对进去,就会得到结果是(2)。所以在ConcurrentHashMap中区别不出来是哪种情况。为了避免这个尴尬,就干脆让value不能为null了。
在遍历ConcurrentHashMap时,它不会把整个集合锁定以保证看到集合的绝对一致的视图。变化发生在已经遍历过的部分,本次遍历不会体现出来。
LinkedHashMap是在HashMap的基础上(所以它的kv都可以为null),通过额外维护一个双向链表来保证元素的顺序。
LinkedHashMap支持两种顺序(插入顺序和访问顺序,要哪种顺序,可用LinkedHashMap的构造函数的参数来指定)。
LinkedHashMap的用途:
LinkedHashMap实现LRU的方法:定义一个LRU类继承LinkedHashMap,重写removeEldestEntry方法,当元素数量超过指定容量时,删除最久未使用的元素。
线程安全的hash表,它给整个数组加了一把大锁,性能比ConcurrentHashMap就低很多了(1.7分段锁、1.8CAS+synchronized,锁粒度更细)。Hashtable不允许键和值存在null。
元素的“key”是有序的,不允许key为空,value可以为空。
底层数据结构是红黑树。
TreeMap是按key排序的,如果key可以为空,当插入多个元素时,如何确定null键在排序中的位置?是放在最前面、最后面,还是其他位置?所以key不能为空。
可以理解HashSet就是包了HashMap,只用到了HashMap里的key,其value是一个固定的值。
HashSet里的元素是无序的。
1. hashset的add方法中,为什么要在map.put的val上放上一个object类型的静态常量present?
为了知道add进元素前,是不是已经有一个相同的值了。
2. hashset的remove方法中,为什么要在map.remove key完了之后要和present进行一个等值比较呢?
为了知道remove的时候,是不是真的remove掉了东西。
可以理解TreeSet就是包了TreeMap,不允许key为空,只用到TreeMap的key,其value是一个固定的值。
TreeSet是SortedSet接口的一个实现类,基于红黑树实现,插入、删除和查找的时间复杂度是O(log n)。
特点:
速记:
- Tree为了排序所以k不能空
- 为了线程安全的kv都不能空,怕其他线程捣乱(HashTable、ConcurrentHashMap)