HashMap、ConcurrentHash面试题

HashMap

首先介绍hashMap是线程不安全的,HashMap是数组+链表+红黑树(JDK1.8)实现的
HashMap中几个重要变量如下:

//默认的初始化容量为16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
//这是为了避免,数组扩容和树化阈值之间的冲突。

HashMap中put方法分析

  1. 进入到put方法之后,首先会将你给定的key通过hash算法以及与运算的方法将其运算得出数组的下标

  2. 如果数组的下标位置元素为空,则将对应的key value 封装成一个对象放入该数组位置,其实也就是存放key value键值对的对象而已

  3. 如果数组下标元素不为空的话,(JDK1.8)
    (1)如果该位置上的key与插入元素的key相等且不为null,则更新数组下标元素
    (2)如果Node节点是红黑树节点,会将key value封装成一个红黑树的Node节点将其添加到红黑树中,并且在这个过程中会判断红黑树是否存在要插入的key,如果存在该key,则直接更新value即可
    (3)如果该位置上Node节点的类型是链表的话,同样将该key value 封装成一个链表Node节点。然后使用尾插法的方式插入到该链表的最后位置中,在进行遍历的时候,同样会遍历链表的key值,如果存在插入的key值的话,那么直接更新value值即可。如果不存在就插入到链表的最后一个位置上。插入到链表之后,会将链表的长度更新,如果链表长度大于等于8的话,会将该链表更新会红黑树。

  4. 将key value封装成Node对象将其插入到链表或者红黑树中后,在判断是否需要扩容,如果需要就进行扩容的操作,如果不需要扩容那么就退出put方法。返回执行的操作。
    在这里插入图片描述

put方法源码


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // hashMap初始数组大小 使用二进制更快因为计算机内部就是二进制
static final float DEFAULT_LOAD_FACTOR = 0.75f;  //负载因子(进行扩容) 当前数组大小>DEFAULT_INITIAL_CAPACITY*DEFAULT_LOAD_FACTOR
  static final int TREEIFY_THRESHOLD = 8;//阈值 当链表长度达到阈值时进行重构为红黑树(平均效率为logn 比较稳定)
  
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
	
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                boolean evict) {
     Node<K,V>[] tab; Node<K,V> p; int n, i;
	 //判断当前的数组是否已经进行了初始化
     if ((tab = table) == null || (n = tab.length) == 0)
				//没有初始化  我去初始化我的空间,or 扩容我的数组
         n = (tab = resize()).length;
		 //tab代表当前Node数组 
		 // 计算需要存放的下标(0 ~(n-1))index=i = (n - 1) & hash
     if ((p = tab[i = (n - 1) & hash]) == null) //代表当前index没有存储元素
         tab[i] = newNode(hash, key, value, null);  //存入新值
	 else {
		//如果 i位 已经被占了  判断当前元素key是否已经存在  
         Node<K,V> e; K k;
         if (p.hash == hash &&
             ((k = p.key) == key || (key != null && key.equals(k))))
             e = p;    //如果存在替换旧值
		//判断数组上悬挂的数据结构是 红黑树 还是链表?
         else if (p instanceof TreeNode)
             //如果是红黑树  直接往树里面插入元素(树会进行调整)
			 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         else {
		 //如果不是树,遍历当前的链表
             for (int binCount = 0; ; ++binCount) {
					//找到节点为null的进行插入链表节点
				 if ((e = p.next) == null) {
                     p.next = newNode(hash, key, value, null);
                     //如果链表长度达到8或大于8 将此链表转为红黑树
					 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                         treeifyBin(tab, hash);
                     break;
                 }
				 //判断是否有重复的元素
                 if (e.hash == hash &&
                     ((k = e.key) == key || (key != null && key.equals(k))))
                     break;
                 p = e;
             }
         }
         if (e != null) { // existing mapping for key
			//替换旧值
             V oldValue = e.value;
             if (!onlyIfAbsent || oldValue == null)
                 e.value = value;
             afterNodeAccess(e);
             return oldValue;
         }
     }
     ++modCount;  //迭代器遍历的时候会用到此数据
	 //如果size>threshold阈值 则扩容
     if (++size > threshold)
         resize();
     afterNodeInsertion(evict);
     return null;
 }

注意:

  1. :在put方法中解决hash碰撞的方式很清楚,即当两个entry的hash值相同时,需要对key值是否相同进行判断,只有key和hash都相同,才能进行修改,否则认为不是同一个entry。
  2. :在两个线程同时进行put时可能造成一个线程数据的丢失
  3. :链表长度>8 并数组大小>=64时 则才会转为红黑树(数组capacity不能太小)
  4. 在JDK1.7以及前是在头结点插入的,在JDK1.8之后是在尾节点插入的。

HashMap多线程不安全,能举出例子来吗?

  1. 第一,如果多个线程同时使用put方法添加元素。假设正好存在两个线程在put时,它们的key发生了碰撞(hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。
      假设线程1和线程2同时执行put,线程1执行put(“1”, “A”),线程2执行put(“5”, “B”),假如都冲突在table[1]这里了
      在正常情况下
    请添加图片描述
    下面来看看异常情况
    两个线程都执行到 if ((p = tab[i = (n - 1) & hash]) == null)这句代码是。
    此时假设线程1 先执行tab[i] = newNode(hash, key, value, null),那么table会变成如下状态:
    请添加图片描述
    紧接着线程2也执行tab[i] = newNode(hash, key, value, null),此时table会变成如下状态:
    请添加图片描述
    这样一来,元素A就丢失了。

  2. 第二,如果多个线程同时检测到元素个数超过数组大小。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。

  3. 第三 put和get并发时,可能导致get为null
    线程1执行put时,因为元素个数超出threshold而导致resize,线程2此时执行get,有可能导致这个问题。
    在进行扩容时,resize方法中首先会将实例变量table赋值给oldTab,之后用新计算的容量new了一个新的hash表,然后将新创建的空hash表赋值给实例变量table。注意此时实例变量table是空的,如果此时另一个线程执行get,就会get出null。
    (最后是通过遍历oldTab来给新的Hash表进行赋值)

ConcurrentHash

  • ConcurrentHash是线程安全的HashMap,
  • synchronizedMap也是线程安全的HashMap,一般不用。因为是采用synchronized方法上加锁,使用阻塞同步,效率低。

JDK1.7 ConcurrentHash

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:
在这里插入图片描述
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样

JDK1.8 ConcurrentHash

  • 和 1.8 HashMap 结构类似,也是数组+链表+红黑树的。取消了Segment 分段锁
  • 红黑树转换条件和HashMap 一模一样:链表长度超过 8并且数组(桶)的长度超过 64请添加图片描述

为什么ConcurrentHash是保证线程安全的?

是采用CAS + synchronized + volatile 来保证并发安全性

  1. 对当前的table进行无条件自循环直到put成功(cas自旋)。
casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))
  1. 如果数组下标没有Node节点,就用CAS+自旋添加链表头节点。
  2. 如果有Node节点,就加synchronized,然后再添加链表或红黑树节点。
  3. get操作,由于数组被volatile修饰了,因此不用担心数组的可见性问题。
volatile Node<K,V>[] table;

ConcurrentHashd的put方法源码如下

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //1. 计算key的hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //2. 判断Node[]数组是否初始化,没有则进行初始化操作
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //3. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        //4. 检查到内部正在扩容,就帮助它一块扩容。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        // 如果该坐标Node不为null且没有正在扩容,就加锁,进行链表/红黑树 节点添加操作
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //5. 当前为链表,在链表中插入新的键值对
                    if (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;
                            }
                        }
                    }
                    // 6.当前为红黑树,将新的键值对插入到红黑树中
                    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;
                        }
                    }
                }
            }
            // 7.判断链表长度已经达到临界值8(默认值),当节点超过这个值就需要把链表转换为树结构
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容 
    addCount(1L, binCount);
    return null;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值