hashmap面试题

1 map 里面那些是线程安全的?

线程安全的:  hashTable , concurrentHashMap, synchronizedMap
线程不安全的: hashMap

2 HashMap的底层是什么?工作原理是什么?

底层: 链表 + 数组。
每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长, 即扩容。

工作原理:
HashMap是基于hashing的原理,我们通过put()和get()方法储存和获取对象。
当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,
让后找到bucket位置来储存值对象。
当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。
HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点。
HashMap在每个链表节点中储存键值对对象。

在这里插入图片描述
3 既然HashMap线程是不安全的, 在多线程中使用会有什么结果?
又是怎么实现线程安全的?

HashMap 是非线程安全的, 一般只在单线程中使用。在多线程条件下,容易导致死循环,具体表现为CPU使用率100%。
因此多线程环境下保证 HashMap 的线程安全性,主要有如下几种方法:

使用 java.util.Hashtable 类,此类是线程安全的。
使用 java.util.concurrent.ConcurrentHashMap,此类是线程安全的。
使用 java.util.Collections.synchronizedMap() 方法包装 HashMap object,得到线程	安全的Map,并在此Map上进行操作。
自己在程序的关键代码段加锁,保证多线程安全(不推荐)

4 我们能否让HashMap同步?

HashMap可以通过下面的语句进行同步:
Map m = Collections.synchronizeMap(hashMap);

5 为什么HashMap是线程不安全的?

1.hash碰撞,同一时刻多个线程操作hashmap并执行put操作,有两个hash值相同的key,
要插入在链表的头结点,可能后插入的覆盖前面的
put和addentry方法都不是同步的
2.删除key:多个线程操作同一个位置,也都会先取的头结点,然后进行计算,
之后再把结果写到该数组位置去,这时候可能已经修改过了,就会覆盖其它线程的修改
3.扩容resize,多个线程同时resize操作,各自生成的新数组并且rehash后赋值给map底层数组table,
最终只有最后一个线程生成的数组给table复制,其他线程都会丢失,
而且当有的线程已经完成而这个线程刚开始时候,就会用已经赋值完的table当做原始数组

6 HashMap怎么实现扩容的?

 当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),
 所以为了提高查询的效率,就要对hashmap的数组进行扩容,

 数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,
 很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,
 而在hashmap数组扩容之后,最消耗性能的点就出现了:
 原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 

7 那么HashMap什么时候进行扩容呢?扩容有哪些缺点?

当hashmap中的元素个数超过数组大小*loadFactor(加载因子实际大小)时,就会进行数组扩容,
loadFactor 的默认值为0.75,也就是说,默认情况下,数组初始大小为16,那么
当hashmap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍。
然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作。

所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。
比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,
不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 
但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 
也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,
既考虑了&的问题,也避免了resize的问题。 

8 hashmap ,hashtable, concurrentHashmap之间有什么异同点?

(1)hashmap和hashtable
A 继承的父类不一样
	Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口。
B 线程安全性不同
	hashmap和hashtable 比较相似的, 只是hashmap没有被synchronized 修饰,是线程不安全的, 而hashtable 被synchronized 修饰, 是线程安全的。

(2)hashtable 与concurrentHashmap

hashtable采用synchronized,而concurrenthashmap分段式锁设计,
提高并发下的处理能力,默认并发度16,并发度指的是程序运行时候能够同时更新map
且不产生锁竞争的最大线程数,不允许null值。

JDK6,7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度,
把HashMap分割成若干个Segment,在put的时候需要锁住Segment,get时候不加锁,
使用volatile来保证可见性,当要统计全局时(比如size),首先会尝试多次计算
modcount来确定,这几次尝试中,是否有其他线程进行了修改操作,如果没有,
则直接返回size。如果有,则需要依次锁住所有的Segment来计算。

(3) hashMap和concurrentHashMap

A 线程安全性
  hashmap 线程不安全, concurrenthashMap 线程安全.
B 迭代器
	ConcurrentHashMap迭代器是弱一致性,hashmap迭代器是强一致性。

9 HashMap 可以被克隆吗

HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,
能被克隆。

10 Object类中有hashCode() 和equals()方法,为什么要同时重写hashCode()和equals()

因为hashCode()和equal()相当于从两个维度(数组 + 链表)来判断这个对象的位置.
如果只重写了其中一个, 返回true, 也不能获取到这个对象在内存中的具体位置.

11 map里面的键值对可以为null?

不一定
hashmap 里面的键值对,都可以null,而且hashMap的key值是Object类,可以是任意类型
hashtable 不行. 如果key 或value == null,会抛出空指针异常.

此处可参考: https://blog.csdn.net/codeHaoHao/article/details/85392932

12 HashMap 和 TreeMap 的区别?

1 继承或实现类不同
	HashMap 继承 AbstractMap 类
	TreeMap 继承  SortedMap类
	
2HashMap:基于哈希表实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。
	(1)HashMap(): 构建一个空的哈希映像
	(2)HashMap(Map m): 构建一个哈希映像,并且添加映像m的所有映射
	(3)HashMap(int initialCapacity): 构建一个拥有特定容量的空的哈希映像
	(4)HashMap(int initialCapacity, float loadFactor): 构建一个拥有特定容量和加载因子的空的哈希映像
	
TreeMap:基于红黑树实现。TreeMap没有调优选项,因为该树总处于平衡状态。
	(1)TreeMap():构建一个空的映像树
	(2)TreeMap(Map m): 构建一个映像树,并且添加映像m中所有元素
	(3)TreeMap(Comparator c): 构建一个映像树,并且使用特定的比较器对关键字进行排序
	(4)TreeMap(SortedMap s): 构建一个映像树,添加映像树s中所有映射,并且使用与有序映像s相同的比较器排序
	
3.Map性能
	HashMap:适用于在Map中插入、删除和定位元素。
	Treemap:适用于按自然顺序或自定义顺序遍历键(key)。

13 Map的遍历方法

1 通过map.keySet()获取到值,然后根据键获取到值
  for(String s:map.keySet()){
            System.out.println("key : "+s+" value : "+map.get(s));
   }
     
2通过Map.Entry(String,String) 获取,然后使用entry.getKey()获取到键,通过entry.getValue()获取到值
  for(Map.Entry<String, String> entry : map.entrySet()){
            System.out.println("键 key :"+entry.getKey()+" 值value :"+entry.getValue());
   }
   
3 通过Iterator遍历获取,然后获取到Map.Entry<String, String>,再得到getKey()和getValue()
     Iterator<Map.Entry<String, String>> it=map.entrySet().iterator();
     while(it.hasNext()){
           Map.Entry<String, String> entry=it.next();
           System.out.println("键key :"+entry.getKey()+" value :"+entry.getValue());
     }

 4 分别遍历键或者值,通过加强for循环
     for(String s1:map.keySet()){//遍历map的键
         System.out.println("键key :"+s1);
     }
     for(String s2:map.values()){//遍历map的值
        System.out.println("值value :"+s2);
     }

14 JDK1.8中, 对ConcurrentHashMap做了哪些优化

JDK1.8中的ConcurrentHashMap: 

	从JDK7版本的 ReentrantLock + Segment + HashEntry,
	到JDK8版本中 synchronized + CAS + HashEntry + 红黑树。
	使用了CAS+synchronized的方式替换了老版本中使用分段锁(ReentrantLock)的方式.
	CAS: compareAndSet

在这里插入图片描述

(1) 数据结构:取消了Segment分段锁的数据结构,取而代之的是Node数组+链表+红黑树的结构,
从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

(2) 保证线程安全机制:JDK7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。
	JDK8采用CAS(读)+Synchronized(写)保证线程安全。
	
(3) 锁的粒度:原来是对需要进行数据操作的Segment加锁,JDK8调整为对每个数组元素加锁(Node)。

(4) 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,
因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。

(5) 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

(6 )JDK8推荐使用mappingCount方法而不是size方法获取当前map表的大小,
    因为这个方法的返回值是long类型,size方法是返回值类型是int。

此处参考:https://www.jianshu.com/p/31f773086e98

15 ConcurrentHashMap一定线程安全么?

ConcurrentHashMap使用不当,也会造成死循环的。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值