面试——ConcurrentHashMap

面试——ConcurrentHashMap

ConcurrentHashMap是J.U.C(java.util.concurrent包)的重要成员,它是HashMap的一个线程安全的支持高效并发的版本

1. 数据结构

JDK1.7版本

  1. 如图所示,ConcurrentHashMap本质上是一个Segment数组,而一个Segment实例则是一个小的哈希表,一个Segment实例又包含若干个桶,每个桶中都包含一条由若干个HashEntry 对象链接起来的链表。

  2. 一个ConcurrentHashMap中只有一个Segment<K,V>类型的segments数组,每个segment中只有一个HashEntry<K,V>类型的table数组,table数组中存放一个HashEntry节点在这里插入图片描述

2. JDK1.8版本

  1. 1.8版本放弃了Segment,跟HashMap一样,用Node描述插入集合中的元素。但是Node中的val和next使用了volatile来修饰,保存了内存可见性。与HashMap相同的是,ConcurrentHashMap1.8版本使用了数组+链表+红黑树的结构。
 static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     volatile V val;
     volatile Node<K,V> next;

     Node(int hash, K key, V val, Node<K,V> next) {
         this.hash = hash;
         this.key = key;
         this.val = val;
         this.next = next;
     }
  1. 同时,ConcurrentHashMap使用了CAS+Synchronized保证了并发的安全性
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());//更为分散的hash值
    int binCount = 0;//统计节点个数
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();//初始化数组
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//该位置没有元素,则用cas自旋获锁,存入节点
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);//如果ConcurrentHashMap正在扩容,则协助其转移
        else {
            V oldVal = null;
            synchronized (f) {//对根节点上锁
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {//fh>=0 说明是链表,否则是红黑树
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);//尾插法插入
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {//红黑树
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {//判断链表的值是否大于等于8,如果大于等于8就升级为红黑树。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

2. Segment

  1. Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色。
  2. 当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁

3. HashEntry

  1. 内部结构:
static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value; //加了volatile修饰,保存内存可见性及防止指令重排
    volatile HashEntry<K,V> next;

    HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
  1. 在插入ConcurrentHashMap元素时,先尝试获得Segment锁,先是自旋获锁,如果自旋次数超过阈值,则转为ReentrantLock上锁
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
     HashEntry<K,V> node = tryLock() ? null :
         scanAndLockForPut(key, hash, value); //自旋获锁
     V oldValue;
     try {
         HashEntry<K,V>[] tab = table;
         int index = (tab.length - 1) & hash;//算出插入位置
         HashEntry<K,V> first = entryAt(tab, index);
         for (HashEntry<K,V> e = first;;) {
             if (e != null) {
                 K k;
                 if ((k = e.key) == key ||
                     (e.hash == hash && key.equals(k))) {//判断插入的元素是否已存在
                     oldValue = e.value;
                     if (!onlyIfAbsent) {
                         e.value = value;
                         ++modCount;
                     }
                     break;
                 }
                 e = e.next;//不存在则遍历下一个
             }
             else {
                 if (node != null)
                     node.setNext(first);
                 else
                     node = new HashEntry<K,V>(hash, key, value, first);//创建节点
                 int c = count + 1;
                 if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                     rehash(node);//扩容
                 else
                     setEntryAt(tab, index, node);//存入节点
                 ++modCount;
                 count = c;
                 oldValue = null;
                 break;
             }
         }
     } finally {
         unlock();//释放锁
     }
     return oldValue;
 }

4. 如何实现扩容?

ConcurrentHashMap 的扩容是仅仅和每个Segment元素中HashEntry数组的长度有关,但需要扩容时,只扩容当前Segment中HashEntry数组即可。也就是说ConcurrentHashMap中Segment[]数组的长度是在初始化的时候就确定了,后面扩容不会改变这个长度。
在这里插入图片描述

5. CAS是什么

1. 概念

  1. Compare and Swap===》比较并替换
  2. CAS属于乐观锁——没有上任何锁,所以线程不会阻塞,但依然会有上锁的效果
  3. CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B,更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

2. 举例:

1. 在内存地址V当中,存储着值为10的变量
2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11。
3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为**自旋**。
6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

3. 缺点

  1. CPU开销过大:在高并发的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
  2. 不能保证代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
  3. ABA问题
    1. 假设内存中有一个值为A的变量,存储在地址V中

    2. 此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值
      在这里插入图片描述

    3. 接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。

    4. 在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。

    5. 最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。
      在这里插入图片描述

4. 解决ABA问题

怎么解决呢?加个版本号就可以了

  1. 真正要做到严谨的CAS机制,在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。
    在这里插入图片描述

  2. 这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。
    在这里插入图片描述

  3. 随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。
    在这里插入图片描述

  4. 在数据库层面操作版本号:判断原来的值和版本号是否匹配,中间有别的线程修改,值可能相等,但是版本号100%不⼀样

update a set value = newValue, vision = vision + 1 where value = #{oldValue} and vision = #{vision}

6. ConcurrentHashMap效率为什么高?

  1. 因为ConcurrentHashMap的get方法并没有上锁(由于HashEntry的value属性使用了volatile修饰,保证了内存可见性,每次获取都是最新值。因此整个过程不需要加锁。)。get时通过hash(key)定位到Segment上,再通过一次Hash定位到具体的HashEntry上。
  2. HashEntry的get方法如下:
public V get(Object key) {// key由equals()确定唯一性
        Segment<K, V> s; // 
        HashEntry<K, V>[] tab;
        int h = hash(key);//h是key的hashcode二次散列值。 根据key的hashcode再做散列函数运算
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//散列算法定位segement,u就是Segement数组的索引,Segment的散列运算,为了将不同key分散在不同segement.根据h获取segement的index
        if ((s = (Segment<K, V>) UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) {// 如果u对应的segement存在,且segement中的table也存在,则获取table中的value
            for (HashEntry<K, V> e = (HashEntry<K, V>) UNSAFE.getObjectVolatile(tab,
                    ((long) (((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) {
                K k;
                if ((k = e.key) == key || (e.hash == h && key.equals(k)))// 查询到对象相同或者equals相等的key则返回对应的value
                    return e.value;
            }
        }
        return null;
    }

7. 什么是fail-safe和fail-fast

  1. fail-safe:安全失败。java.util.concurrent并发包下的容器都是遵循安全失败机制。即可以在多线程下并发修改。不会抛出并发修改的异常Concurrent Modification Exception
  2. Fail-fast: 快速失败。Java集合在使用迭代器遍历时,如果遍历过程中对集合中的内容进行了增删改的操作时,则会抛出并发修改的异常Concurrent Modification Exception。即使不存在并发,也会抛出该异常,所以称之为快速失败。
    @Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next)
                    action.accept(e.key, e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值