微信公众号:放开我我还能学java
分享知识,共同进步!web
看你简历里写了 HashMap,那你说说它存在什么缺点?数组
那你有用过线程安全的 Map 吗?安全
有,回答在哪用过。微信
没有,不过我了解过。多线程
那你说说它们的实现。并发
Hashtableapp
Hashtable 自己比较低效,由于它的实现基本就是将 put、get、size 等各类方法加上 synchronized 锁。这就致使了全部并发操做都要竞争同一把锁,一个线程在进行同步操做时,其余线程只能等待,大大下降了并发操做的效率。编辑器
Collections#SynchronizedMapide
同步包装器 SynchronizedMap 虽然没使用方法级别的 synchronized 锁,可是使用了同步代码块的形式,本质上仍是没有改进。
ConcurrentHashMap
首先 ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的实现方式是不一样的。
在 JDK1.7 中,ConcurrentHashMap 是由 Segment
数组结构和 HashEntry
数组结构组成。Segment 继承了 ReentrantLock,是一种可重入锁。HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组 ,每一个 HashEntry 是一个链表结构的元素,所以 JDK1.7 的 ConcurrentHashMap 是一种数组+链表结构。当对 HashEntry 数组的数据进行修改时,必须首先得到与它对应的 Segment 锁,这样只要保证每一个 Segment 是线程安全的,也就实现了全局的线程安全(分段锁)。
在 JDK1.8 中,ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构,在锁的实现上,采用 CAS 操做和 synchronized 锁实现更加低粒度的锁,将锁的级别控制在了更细粒度的 table 元素级别,也就是说只须要锁住这个链表的首节点,并不会影响其余的 table 元素的读写,大大提升了并发度。
那为何 JDK1.8 要使用 synchronized 锁而不是其余锁呢?
我认为有如下两个方面:
你提到了 synchronized 的锁状态升级,能具体说下每种锁状态在什么状况下升级吗?
无锁
无锁没有对资源进行锁定,全部的线程都能访问并修改同一个资源,但同时只有一个线程能修改为功。
偏向锁
当一段同步代码一直被同一个线程所访问,无锁就会升级为偏向锁,之后该线程访问该同步代码时会自动获取锁,下降了获取锁的代价。
轻量级锁
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其余线程会经过自旋的形式尝试获取锁,不会阻塞,从而提升性能。
重量级锁
若当前只有一个等待线程,则该线程经过自旋进行等待。可是当自旋超过必定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
为何 ConcurrentHashMap 的 key 和 value 不能为 null?
这是由于当经过 get(k)
获取对应的 value 时,若是获取到的是 null 时,没法判断,它是 put(k,v)
的时候 value 为 null,仍是这个 key 历来没有添加。
假如线程 1 调用 map.contains(key)
返回 true,当再次调用 map.get(key)
时,map 可能已经不一样了。由于可能线程 2 在线程 1 调用 map.contains(key)
时,删除了 key,这样就会致使线程 1 获得的结果不明确,产生多线程安全问题,所以,ConcurrentHashMap 的 key 和 value 不能为 null。
其实这是一种安全失败机制(fail-safe),这种机制会使你这次读到的数据不必定是最新的数据。
那你谈谈快速失败(fail-fast)和安全失败(fail-safe)的区别。
快速失败和安全失败都是 Java 集合中的一种机制。
若是采用快速失败机制,那么在使用迭代器对集合对象进行遍历的时候,若是 A 线程正在对集合进行遍历,此时 B 线程对集合进行增长、删除、修改,或者 A 线程在遍历过程当中对集合进行增长、删除、修改,都会致使 A 线程抛出 ConcurrentModificationException 异常。
为何在用迭代器遍历时,修改集合就会抛异常时?
缘由是迭代器在遍历时直接访问集合中的内容,而且在遍历过程当中使用一个 modCount 变量。集合在被遍历期间若是内容发生变化,就会改变 modCount 的值。
每当迭代器使用 hashNext()/next() 遍历下一个元素以前,都会检测 modCount 变量是否为 expectedModCount 值,是的话就返回遍历;不然抛出异常,终止遍历。
java.util 包下的集合类都是快速失败的。
若是采用安全失败机制,那么在遍历时不是直接在集合内容上访问,而是先复制原有集合内容,在拷贝的集合上进行遍历。
因为迭代时是对原集合的拷贝进行遍历,因此在遍历过程当中对原集合所做的修改并不能被迭代器检测到,故不会抛 ConcurrentModificationException 异常。
java.util.concurrent 包下的并发容器都是安全失败的。
ConcurrentHashMap 的 get 方法为何不用加锁,会不会出现数据读写不一致状况呢?
不会出现读写不一致的状况。
get 方法逻辑比较简单,只须要将 key 经过 hash 以后定位到具体的 Segment ,再经过一次 hash 定位到具体的元素上。
因为变量 value
是由 volatile
修饰的,根据 JMM 中的 happen before
规则保证了对于 volatile 修饰的变量始终是写操做先于读操做的,而且 volatile 的内存可见性保证修改完的数据能够立刻更新到主存中,因此能保证在并发状况下,读出来的数据是最新的数据。
说下 ConcurrentHashMap 的 put 方法执行逻辑。
JDK1.7:
先尝试自旋获取锁,若是自旋重试的次数超过 64 次,则改成阻塞获取锁。获取到锁后:
JDK1.8:
先定位到 Node,拿到首节点 first,判断是否为:
first.hash = MOVED = -1
,说明其余线程在扩容,参与一块儿扩容。
first.hash != -1
,synchronized 锁住 first 节点,判断是链表仍是红黑树,遍历插入。
说下 ConcurrentHashMap 的 size 方法如何计算最终大小。
JDK1.7:
虽然 count
变量是被 volatile
修饰的,可是并非简单的把全部 Segment 的 count 值相加。由于有可能在累加过程当中 count 值发生了改变,那么此时结果就不正确了。可是也不能直接锁住,这样效率过低。所以在 JDK1.7 中的作法是先尝试 2 次经过不锁住 Segment 的方式来统计各个 Segment 大小,若是统计的过程当中,容器的 count 发生了变化,则再采用加锁的方式来统计全部Segment 的大小。
那么 ConcurrentHashMap 是如何判断在统计的时候容器是否发生了变化呢?使用modCount
变量,在put
、 remove
方法里操做元素前都会将变量 modCount 进行加 1,那么在统计大小先后比较 modCount 是否发生变化,从而得知容器的大小是否发生变化。
JDK1.8:
因为没有 segment 的概念,因此只须要用一个 baseCount
变量来记录 ConcurrentHashMap 当前节点的个数。
counterCells
里,后续数组 counterCells 的值会加到 baseCount 中。
如何提升 ConcurrentHashMap 的插入效率?
主要从如下两个方面入手:
ConcurrentHashMap 是否存在线程不安全的状况?若是存在的话,在什么状况下会出现?如何解决?
我想起来了,存在这种状况,请看以下代码
public class ConcurrentHashMapNotSafeDemo implements Runnable {
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>(); public static void main(String[] args) throws InterruptedException { scores.put("John", 0); Thread t1 = new Thread(new ConcurrentHashMapNotSafeDemo()); Thread t2 = new Thread(new ConcurrentHashMapNotSafeDemo()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(scores); } @Override public void run() { for (int i = 0; i < 1000; i++) { Integer score = scores.get("John"); Integer newScore = score + 1; scores.put("John", newScore); } } } 复制代码
结果输出 {John=1323}
,很明显发生了错误。
缘由就在于这三行在一块儿不是线程安全的,虽然 get 方法和 put 方法是线程安全的,可是中间的又对获取的值修改了,所以致使线程不安全。
解决方法是使用 replace 方法
能够看到,replace 方法传入三个值,分别是当前 key、旧值、新值
最终修改以下:
获取更多最新文章,关注公众号【放开我我还能学】