HashMap面试题

ArrayList的问题

  1. 请问当我new 一个 ArrayList< Integer >() 里面用的什么?
    一个数组
  2. 什么类型的数组?
    new 了一个新的 list 初始值为10
  3. 10的体现在哪?
    如果不够了,扩容10的一半也就是5,总共就是15
  4. ArrayList是线程安全的还是线程不安全的?
    不安全,因为它的add方法写操作的时候没有加锁。
  5. 举一个线程不安全的例子
    在单线程中一般不会出错,但是在多线程中会报java.Util.ConcurrentModificationExcepion (并发修改异常)
  6. 该异常的解决?
    故障现象:
      java.Util.ConcurrentModificationExcepion
    导致原因:
      并发争抢修改导致,参考花名册签名情况。一个人正在写入,另外一个同学过来抢夺,导致数据不一致异常。
    解决方案:
       1.List list=new Vector()可以解决问题,一致性得到解决,但是并发性急剧下降。
       2.List list=Collections.synchronizedList(new ArrayList<>())
       3.List list=new CopyOnWriteArrayList<>(); 俗称写实复制,读写分离
    优化建议(同样的错误不犯第二次):

HashSet问题

  1. HashSet是否是线程安全的?
    不安全
  2. 如何解决?
    Set< String >=Collections.synchronizedSet(new HashSet<>());
  3. HashSet底层是什么?
    HashSet的底层结构就是HashMap
  4. HashSet只需要一个值,但是HashMap是两个值,怎么解释?
    在add方法中确实是map.put但是value上面是定义的常量,在HashSet中,只关心key值,不关心value值。

HashMap

  1. HashMap在多线程存在线程安全问题,如何解决?
    使用Collections.synchronizedMap(Map)创建线程安全的map集合
    HashTable
    ConcurrentHashMap
    不过出于线程并发度的原因,我都会舍弃前两者使用最后的ConcurrentHashMap,它的性能和效率明显的高于前两者。
  2. Collections.synchronizedMap是怎么实现线程安全的?
    在SynchronizedMap内维护了一个普通对象Map,还有排斥锁mutex,如图
    在这里插入图片描述
    cllections.synchronizedMap,可以看到有两个构造器,如果传入了mutex参数,则将对象排斥锁赋值为传入的对象。
    如果没有,则将对象排斥锁赋值为this,即调用synchronizedMap对象,就是上面的Map。
    创建出synchronizedMap之后,再操作map的时候,就会对方法上锁,如图全是锁
    在这里插入图片描述

HashTable、HashMap

  1. 说一个HashTable和HashMap
    跟HashMap相比HashTable是线程安全的,适合在多线程的情况下使用,但是效果可不太乐观。
  2. 为什么说它效率低。
    因为我看源码的时候,他在对数据库操作的时候都会上锁,所以效率比较低下。
    在这里插入图片描述
  3. 除了这个HashMap跟HashTable还有什么不一样的点?
    Hashtable 是不允许键值为null的,HashMap的键值规则都可以为null
  4. 为什么HashTable是不允许key和value为null,而HashMap则可以呢?
    因为HashTable在我们put空值得时候会直接抛出空指针异常,但是HashMap做了特殊的处理。
static final int hash(Object key){
	int h;
	return (key==null)?0:(h=key.hashCode())^(h>>>16);
}
  1. 你还是没有说清楚Hashtable是不允许键或者值为null,hashMap可以
    因为Hashtable使用得是安全失败机制(fail-safe),这种机制会使你此次读到得数据不一定是最新得数据,如果你使用null值,就会使得其无法判断对应得key是不存在还是为空,因为你无法调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理
  2. 你还知道哪些不用点
    实现方式不同:Hashtable继承了Dictonary类,而HashMap继承得是AbstractMap类
    初始化容量不同:HashMap得初始容量为:16,Hashtable得初始容量为:11,两者得负载因子默认都是0.75
    扩容机制不同:当现有容量大于总容量*负载因子时,HashMap扩容规则为当前容量翻倍,Hashtable扩容规则为当前容量翻倍+1
    迭代不同:HashMap中的Iterator迭代器是fail-fast的,而Hashtable得Enumerator不是fail-fast的
    所以当其他线程改变了HashMap的结构,如:增加、删除元素,将会抛出ConcurrentModificationException异常,而HashTable不会
  3. fail-fast是什么?
    加速失败,是java集合中的一种机制,在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象内容进行了修改(增加、删除、修改)则会抛出ConcurrentModificationException
  4. fail-fast的原理是什么?
    迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量
    集合在被遍历期间如果内容发生变化,就会改变modCount的值
    每当迭代器使用hashNext()/next()遍历下一个元素之前,就会检测modCount变量是否为exceptedmodCount值,使得话就返回遍历,否则抛出异常终止遍历。
    Tip:这里的异常的抛出条件是检测到modCount!=expectionmodCount这个条件。如果集合发生变化时修改modCount值刚好又设置为expectedmodCount值,则异常不会抛出。
    因此,不能依赖这个异常是否抛出而进行并发操作过程,这个异常只是建议用于检测并发修改bug
  5. 它被用在哪些场景?
    java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代中被修改)算是一种安全机制。
    Tip: 安全失败(fail-safe),java.util.concurrent包下容器都是安全失败,可以在多线程下并发使用,并发修改。

concurrentHashMap连环炮

  1. ConcurrentHashMap的并发度不够,性能很低,那么是怎么处理的?
    我们在开发过程中都是使用ConcurrentHashMap,他的并发相比前两者好很多
  2. 说说它的数据结构吧,以及为什么它的并发度那么高?
    ConcurrentHashMap底层是基于数组+链表组成的,不过在jdk1.7和1.8中具体实现稍微有点不同,先说一下1.7数据结构
    在这里插入图片描述
    如图所示,是由Segment数组、HashEntry组成,和HashTable一样,仍然是数组加链表。Segment是ConcurrentHashMap的一个内部类,主要的组成如下
static final class Segment<K,V> extends ReentrantLock implements Serializable {

    private static final long serialVersionUID = 2249069246763182397L;

    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;

    transient int count;
        // 记得快速失败(fail—fast)么?
    transient int modCount;
        // 大小
    transient int threshold;
        // 负载因子
    final float loadFactor;

}

HashEntry和HashMap差不多,但是不同点是,他使用volatile去修饰了它的数据Value还有下一个节点next

  1. volatile的特性有哪些?
    保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程老说是立即可见的。(实现可见性)
    禁止指令重排(实现有序性)
    volatile只保证对单次读/写的原子性。i++这种操作不保证原子性
    我的这一片博客中有写到volatile更深入的知识。
    https://blog.csdn.net/qmqm33/article/details/105144317
  2. 那你能说说ConcurrentHashMap并发度高的原因吗?
    原理上来说,ConcurrentHashMap采用了分段锁的技术,其中Segment继承于ReentrantLock
    不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrencyLevel(Segment数组数量)的线程并发。
    每当一个线程占用锁访问一个Segment时,不会 影响到其他的Segment
    就是说如果容量大小是16它的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();//这就是为啥他不可以put null值的原因
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          
         (segments, (j << SSHIFT) + SBASE)) == null) 
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

它先定位到Segment,然后进行put操作
我们看看它的put源码,你就知道他是怎么做到线程安全的了

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
          // 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
            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;
 // 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                        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 {
                 // 为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                        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;
        }

首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在的竞争,则利用scanAndLoackForPut()自旋获取锁
1.尝试获取锁
2.如果重试的次数达到了MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功

  1. 那他的get呢
    get逻辑比较简单,只需要将key通过Hash之后定位到具体的Segment,再通过一次Hash定位到具体的元素上
    由于HashEntry 中的 value 属性是用volatlle关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。
    ConcurrentHashMap的get方法是非常高效的,因为整个过程都不需要加锁。

  2. 你有没有发现1.7虽然可以支持每个Segment并发访问,但是还是存在一些问题?
    是的,因为基本上还是数组加链表的方式,我们去查询的时候,还得遍历链表,会导致效率很低,这个跟jdk1.7的HashMap是存在的一样的问题,所以他在jdk1.8完全优化了。

  3. 具体1.8做出了哪些优化?
    其中抛弃了原有的Segment分段锁,而采用了CAS+synchronized来保证并发安全性
    跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)

  4. 同样的,你能聊一下它的取值操作吗,以及是怎么保证线程安全的?
    ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤
    1、根据key计算出hashCode
    2、判断是否需要进行初始化
    3、即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋锁保证成功
    4、如果当前位置的Hashcode==MOVED ==-1 ,则需要进行扩容
    5、如果都不满足,则利用synchronized锁写入数据
    6、如果数量大于 TREEIFY_THRESHOLD 则要转为红黑树

  5. 你在上面提到的CAS是什么?自旋锁又是什么?
    CAS 是乐观锁的一种实现方式,是一种轻量级锁,JUC中很多工具类的实现就是基于CAS的。
    CAS操作的流程如下图所示,线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。
    这是一种乐观策略,认为并发操作并不总会发生
    在这里插入图片描述
    可以参考我下面的整个CAS体系以及深入ABA问题。
    https://blog.csdn.net/qmqm33/article/details/105069211

  6. CAS性能很高的,但是我知道synchronized性能很差,为什么jdk1.8升级之后反而多了synchronized?
    synchronized之前一直都是重量级的锁,但是后来java官方对他进行过升级的,他现在采用的时锁升级的方式去做的。针对synchronized获取锁的方式,JVM使用了锁升级的优化方式,就是先使用偏向锁优先于同一线程然后再次获取锁,如果失败就升级为CAS轻量级锁,如果失败就会短暂自旋,防止线程被挂起。最后如果以上都失败就升级为重量级锁。
    所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

  7. 那我们回归正题,ConcurrentHashMap的get操作又是怎么样子的?
    根据计算出的hashcode寻址,如果就是在桶上那么直接返回值
    如果是红黑树那就按照树的方式获取值
    都不满足那就按照链表的方式遍历获取值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值