问题描述
翻翻别人的面试经历
这里在知乎上看到的,分享出了自己面试阿里Java
岗的面试题。
看了一下,除了Spring
之外的其他很多题都不会,但是不能拿学校没讲Java
作为借口,因为可能讲了也不会。
但是第九个问题,我觉得应该立刻话时间研究研究了,因为之前在缓存中用到了这个。
当时也不明白具体HashMap
和ConcurrentHashMap
究竟有什么区别。
只是记得之前看过有关大数据的场景下利用缓存减轻数据库压力的文章,文中说常用ConcurrentHashMap
,所以这里缓存就用这个了,其实并不懂原理,下面,让我们一起来研究一下。
Map
Map
大家都熟悉了,Java
中也有,JavaScript
中也有。
Map
是一种键值对类型的数据结构,根据键映射到值。
不分析源码了,就把思想给大家讲一下,以下主要以图为主。
HashMap
Java7
HashMap
的本质是一个可变长度的数组,在数组中每个位置保存的是一个Entry
节点,该节点存储有hash
、key
、value
、next
等信息。
Java7
中的HashMap
实现与我们在数据结构中学习的类似,对key
进行hash
,如果冲突了,则添加到链表中。
然后查询的时候就先根据hash
找到相应的位置,然后根据链表逐一比较,返回相应的value
。时间复杂度取决于链表的长度,时间复杂度为O(N)
。
Java8
Java8
中对HashMap
进行了优化,如果链表中元素超过8
个时,就将链表转化为红黑树,以减少查询的复杂度,将时间复杂度降低为O(logN)
。
HashMap
没有对多线程的场景下做任何的处理,不用说别的,就两个线程同时put
,然后冲突了,两者需要操作一个链表/红黑树,这肯定就会有错误发生,所以HashMap
是线程不安全的。
HashTable
HashTable
与Java7
中的HashMap
类似,也是一个数组加链表,不过这个线程安全。
HashTable
线程安全,但是它的线程安全是依赖将所有修改HashTable
的代码块都用synchronized
修饰。
synchronized
关键字我们之前在单例模式中用到过,它修饰的代码块,同一时刻只允许一个线程访问,其他线程会被阻塞,等待该线程执行完再执行。
所以,在HashTable
中,一个线程在put
,其余的线程在get
的时候就会被阻塞,无法并行。所以不推荐使用HashTable
,虽然它线程安全。
ConcurrentHashMap
HashMap
线程不安全,HashTable
性能又不好,当然需要设计一个新类去解决这些问题,这就是ConcurrentHashMap
。
Java7
这是Java7
中实现线程安全的思路,ConcurrentHashMap
由16
个segment
组成,每个segment
就相当于一个HashMap
(数组+链表)。
segment
最多16
个,想要扩容,就是扩充每个segment
中数组的长度。
然后只要实现每个segment
是线程安全的,就让这个Map
线程安全了。每个segment
是加锁的,对修改segment
的操作加锁,不影响其他segment
的使用,所以理想情况下,最多支持16
个线程并发修改segment
,这16
个线程分别访问不同的segment
。
同时,在segment
加锁时,所有读线程是不会受到阻塞的。
这样设计,put
与get
的基本操作就是先找segment
,再找segment
中的数组位置,再查链表。
Java8
后来人们发现Java7
这样设计太复杂了,回归本质。
HashMap
线程不安全的问题完全都是出在对链表/红黑树的操作上,为什么非要建一个segment
加锁呢?直接对链表/红黑树加锁不就好了?
所以Java8
的ConcurrentHashMap
完全就是HashMap
进行加锁,实现线程安全。
这里总结的很简单,其实源码中对这个的实现特别复杂,有兴趣的可以去看看,反正我是看着头大。
总结
-
HashMap
线程不安全。 -
HashTable
线程安全,但性能差,不推荐使用。 -
ConcurrentHashMap
线程安全。 -
ConcurrentHashMap
在Java7
中使用segment
实现,对每个segment
加锁。 -
ConcurrentHashMap
在Java8
中是直接在HashMap
的基础上进行加锁。
参考文献:
Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
ConcurrentHashMap、HashTable、HashMap三兄弟