文章目录
前言
在做mit6.830时,关于数据库的全局目录catalog
类持有一个线程安全的ConcurrentHashMap
去保存数据库中每一张表。为了避免多个线程对数据库目录修改造成数据不一致的情况,是不能使用HashMap
的,这是因为HashMap
是线程不安全的。
一、HashMap
1.JDK1.7和JDK1.8中HashMap底层原理的不同
首先在JDK1.7中HashMap的无参构造会默认创建一个数组长度为16的Entry数组,而在JDK1.8中只有我们在首次进行put操作的时候才会创建一个数组长度为16的Node数组。
在JDK1.7中,是先判断是否到达阈值,再进行插入操作,假设数组容量已经到达了阈值,且此时散列得到的位置不为空,则进行扩容操作;而在JDK1.8中是插入结点后判断链表长度是否大于8,并且数组长度是否小于64,如果小于64,进行数组扩容,如果大于64,将链表转换成红黑树。
2.HashMap为什么线程不安全?
在JDK1.7中,在进行扩容操作的时候,可能会造成环形链表或者数据丢失的情况,这是由于在扩容后会对原数组中的Entry结点重新散列,由于链表插入时采用的是头插法,新的链表的顺序与原来的链表顺序相反,假设有多个线程同时进行put操作,可能会发生环形链表或者数据丢失的情况;
在JDK1.8中,链表的插入是直接在链表尾部插入,避免了环形链表的产生,但HashMap仍然是线程不安全的,因为在并发的情况下,两个线程同时进行put操作,假设它们散列到同一个槽,在线程A进行判断发现没有hash碰撞后,此时CUP时间片耗尽,线程A被挂起,线程B得到CPU时间片同时进行了put操作,操作完成后线程A再次得到时间片,由于已经判断过没有hash冲突,会直接进行put覆盖线程B插入的数据。
面试官:HashMap 为什么线程不安全?
hashmap1.7头插法造成死循环的原因分析
二、ConcurrentHashMap
1.为什么是ConcurrentHashMap而不是HashTable
对于HashMap中线程不安全的问题,可以用HashTable来解决,但是该Map实现会存在无论读还是写,它会给整个集合加锁,导致同一时间的其他操作阻塞,而ConcurrentHashMap兼顾性能和线程安全,一个线程进行写操作时,它会锁住一小部分,其他部分的读写不受影响,其他线程访问没上锁的地方不会被阻塞。
2.JDK1.7和JDK1.8下,ConcurrentHashMap的结构对比
说在最开始,JDK1.7的ConcurrentHashMap是通过分段加锁保证并发;而JDK1.8的ConcurrentHashMap是通过cas加上synchronized的方式保证并发。
在JDK1.7中,ConcurrentHashMap由16个 Segment 组合而成,共同保存在segments 数组中。Segment 本身就相当于一个 HashMap 对象。每个Segment都独立有一把ReentrantLock,保证了并发效率。在Segment内如果发生哈希冲突,同 HashMap 一样,以链表的形式放在冲突槽位上。
在JDK1.8中,ConcurrentHashMap同HashMap一样,对于冲突过长的链表会转换成红黑树。
为什么默认哈希冲突是链表的形式,而链表长度超过8就要红黑树化,又为什么是8呢?
首先因为红黑树的空间占用是链表的两倍,所以最开始默认是链表的形式。根据泊松分布计算,哈希冲突导致链表长度增加到8的概率已经是千万分之一了,所以我们链表的长度一般不会到达这个值,如果到了这个值,我们就红黑树化。
值得注意的是,HashMap中允许一个key值为null,但在ConcurrentHashMap中,key值为null会抛出异常。
在JDK1.8中,ConcurrentHashMap使用put()方法时,计算出哈希值得到槽位后会判断该槽位是否有值,如果没有值会用cas的方法把值放入进去。
如果cas失败,会进入下一层判断,判断是否处于扩容状态,如果是,则帮助进行扩容。
如果该槽位有值,则会用synchronized的方式进行put操作。
3.JDK1.8下,ConcurrentHashMap的put()和get()方法总结
三、ConcurrentHashMap一定是线程安全的吗
结论:一定是。但是如果在多线程并发的情况下,组合操作并不能保证线程安全。如下代码,如果我们的Runnable是在get后又进行了put,两个线程安全的操作组合在一起就变成了线程不安全
怎么解决?
加synchronized吗?固然这种方法可以解决,这样和我们使用HashMap没什么区别了。在ConcurrentHashMap中提供了一个replace()方法,可以让我们进行cas的原子操作,这样利用自旋和cas就可以解决问题了。
package collections.concurrenthashmap;
import java.util.concurrent.ConcurrentHashMap;
/**
* 描述: 组合操作并不保证线程安全
*/
public class OptionsNotSafe implements Runnable {
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<String, Integer>();
public static void main(String[] args) throws InterruptedException {
scores.put("小明", 0);
Thread t1 = new Thread(new OptionsNotSafe());
Thread t2 = new Thread(new OptionsNotSafe());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(scores);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while (true) {
Integer score = scores.get("小明");
Integer newScore = score + 1;
boolean b = scores.replace("小明", score, newScore);
if (b) {
break;
}
}
}
}
}
总结
最开始只是想看看ConcurrentHashMap的底层原理,后来发生衍生出来的一些东西搞不明白,就去进行了递归学习哈哈哈,后面应该还会有类似由MIT6.830引发的一些思考的随笔。