Map-JAVA面试常问

1.HashMap底层实现

底层实现在jdk1.7和jdk1.8是不一样的

jdk1.7采用数组加链表的方式实现 jdk1.8采用数组加链表或者红黑树实现

1.png2.png

HashMap中每个元素称之为一个哈希桶(bucket),哈希桶包含的内容有以下4项

  • hash值(哈希函数计算出来的值)

  • Key

  • value

  • next(下一个节点默认).

默认情况下,在jdk1.8+版本中,HashMap使用的是数组加链表的形式存储的,而当数组的长度大于64,并且链表的长度大于8时,就会将链表升级成红黑树,以增加HashMap的查询时的性能

初始容量:HashMap的初始容量为0,这是一种懒加载的方式,直到第一次put操作才会初始化数组大小默认为16

2.ConcurrentHashMap原理?为什么要这样改进

ConcurrentHashMap在不同的JDK版本中实现也是不同的。

在JDK1.7中它使用的是数组加链表的形式实现的,而数组分为:大数组Segment和小数组HashEntry。 而大数组Segment可以理解为MYSQL中的数据库,而每个数据库(Segment)中又有很多张表HashEntry,每一个HashEntry又有多条数据,这些数据用链表连接的,如下图所示:

33.webp

而在JDK1.7中,ConcurrentHashMap是通过在Segment加锁来保证其安全性的,所以我们把它称为分段锁或片段锁,如下图所示

444.webp

它实现的源码如下:

555.webp

从上面的源码可以看出,JDK1.7时,ConcurrentHashMap主要用ReentrantLock进行加锁来实现线程安全的。 而在JDK1.8中,它是使用了数组+链表/红黑树的方式优化了concurrentHashMap的实现,具体结构如下

666.png

链表升级为红黑树的规则:当链表长度大于8,并且数组的长度大于64时,链表就会升级为红黑树的结构。

注意:ConcurrentHashMap在jdk1.8+虽然保留了Segment的定义,但这只是为了保证序列化时的兼容性,不再有任何结构上的用处了。

在JDK1.8中的ConcurrentHashMap使用的是CAS+volatile或者syncHronized的方式来保证线程安全的,他的核心实现源码如下。

//ConcurrentHashMap使用volatile修饰节点数组,保证其可见性,禁止指令重排。
//而HashMap没有使用volatile,  transient Node<K,V>[] table; 
transient volatile Node<K,V>[] table;
public V put(K key, V value) {return putVal(key, value, false);
}final V putVal(K key, V value, boolean onlyIfAbsent) {// key和value都不能为nullif (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());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未初始化或者个数为0,则初始化node数组tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))// 如果使用CAS插入元素时,发现已经有元素了,则进入下一次循环,重新操作// 如果使用CAS插入元素成功,则break跳出循环,流程结束break;                   // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED)// 如果要插入的元素所在的tab的第一个元素的hash是MOVED,则当前线程帮忙一起迁移元素tab = helpTransfer(tab, f);else {   //发生hash冲突// 如果这个tab不为空且不在迁移元素,则锁住这个tab(分段锁)// 并查找要插入的元素是否在这个tab中// 存在,则替换值(onlyIfAbsent=false)// 不存在,则插入到链表结尾或插入树中V oldVal = null;synchronized (f) {// 再次检测第一个元素是否有变化,如果有变化则进入下一次循环,从头来过if (tabAt(tab, i) == f) {// 如果第一个元素的hash值大于等于0(说明不是在迁移,也不是树)// 那就是tab中的元素使用的是链表方式存储if (fh >= 0) {// tab中元素个数赋值为1binCount = 1;// 遍历整个tab,每次结束binCount加1for (Node<K,V> e = f;; ++binCount) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {// 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false),并退出循环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;// tab中元素个数赋值为2binCount = 2;// 调用红黑树的插入方法插入元素,如果成功插入则返回null,否则返回寻找到的节点if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {// 如果找到了这个元素,则赋值了新值(onlyIfAbsent=false),并退出循环oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}// 如果binCount不为0,说明成功插入了元素或者寻找到了元素if (binCount != 0) {// 如果链表元素个数达到了8,则尝试树化// 因为上面把元素插入到树中时,binCount只赋值了2,并没有计算整个树中元素的个数,所以不会重复树化if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);// 如果要插入的元素已经存在,则返回旧值if (oldVal != null)return oldVal;// 退出外层大循环,流程结束break;}}}// 成功插入元素,元素个数加1(是否要扩容在这个里面)addCount(1L, binCount);// 成功插入元素返回nullreturn null;
}
public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;// 计算hashint h = spread(key.hashCode());// 判断数组是否为空,通过key定位到数组下标是否为空if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 如果第一个元素就是要找的元素,直接返回if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}else if (eh < 0)// hash小于0,说明是树或者正在扩容// 使用find寻找元素,find的寻找方式依据Node的不同子类有不同的实现方式return (p = e.find(h, key)) != null ? p.val : null;// 遍历整个链表寻找元素while ((e = e.next) != null) {if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;
}
public V remove(Object key) {// 调用替换节点方法return replaceNode(key, null, null);
}final V replaceNode(Object key, V value, Object cv) {// 计算hashint hash = spread(key.hashCode());// 循环遍历数组for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;//校验参数if (tab == null || (n = tab.length) == 0 ||(f = tabAt(tab, i = (n - 1) & hash)) == null)break;else if ((fh = f.hash) == MOVED)// 如果正在扩容中,协助扩容tab = helpTransfer(tab, f);else {V oldVal = null;// 标记是否处理过boolean validated = false;//用 synchronized 同步锁,保证并发时元素移除安全synchronized (f) {// 再次验证当前tab元素是否被修改过if (tabAt(tab, i) == f) {if (fh >= 0) {// fh>=0表示是链表节点validated = true;// 遍历链表寻找目标节点for (Node<K,V> e = f, pred = null;;) {K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {V ev = e.val;if (cv == null || cv == ev ||(ev != null && cv.equals(ev))) {oldVal = ev;if (value != null)e.val = value;else if (pred != null)pred.next = e.next;elsesetTabAt(tab, i, e.next);}break;}pred = e;// 遍历到链表尾部还没找到元素,跳出循环if ((e = e.next) == null)break;}}else if (f instanceof TreeBin) {// 如果是树节点validated = true;TreeBin<K,V> t = (TreeBin<K,V>)f;TreeNode<K,V> r, p;// 遍历树找到了目标节点if ((r = t.root) != null &&(p = r.findTreeNode(hash, key, null)) != null) {V pv = p.val;if (cv == null || cv == pv ||(pv != null && cv.equals(pv))) {oldVal = pv;if (value != null)p.val = value;else if (t.removeTreeNode(p))setTabAt(tab, i, untreeify(t.first));}}}}}// 如果处理过,不管有没有找到元素都返回if (validated) {// 如果找到了元素,返回其旧值if (oldVal != null) {// 如果要替换的值为空,元素个数减1if (value == null)addCount(-1L, -1);return oldVal;}break;}}}// 没找到元素返回空return null;
}

从上述代码可以看出,在JDK1.8中,添加元素首先会判断容器是否为空,如果为空则使用volatile加cas来初始化。如何容器不为空则根据存储的元素计算改位置是否为空,如果为空则利用cas设置该节点;如果不为空则使用synchronize加锁,遍历桶中的数据,替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。

put操作总结

做插入操作时,首先进入乐观锁,在乐观锁中判断容器是否初始化,
如果没初始化则初始化容器;如果已经初始化,则判断该hash位置的节点是否为空,
如果为空,则通过CAS操作进行插入。
如果该节点不为空,再判断容器是否在扩容中,如果在扩容,则帮助其扩容。如果没有扩容,则进行最后一步,先加锁,然后找到hash值相同的那个节点(hash冲突),
循环判断这个节点上的链表,决定做覆盖操作还是插入操作。
循环结束,插入完毕。

get操作总结

步骤如下:

  • 判断数组是否为空,通过key定位到数组下标是否为空;
  • 判断node节点第一个元素是不是要找到,如果是直接返回;
  • 如果是红黑树结构,就从红黑树里面查询;
  • 如果是链表结构,循环遍历判断。

ConcurrentHashMapget()方法没有加synchronized锁,为什么可以不加锁?因为tablevolatile关键字修饰,保证每次获取值都是最新的。

【Hashtable的get(Object key)方法加了synchronized锁,性能较差】

总结:我们把上述流程简化一下,可以简单的认为在JDK1.8中,ConcurrentHashMap是在头节点加锁来保证线程安全的,锁的粒度相比JDK1.7的Segment来说就更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了,而且JDK1.8使用的红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询效率也得到了很大的提升,从之前的O(n)优化到了O(logn)的时间复杂度,具体加锁示意图如下所示:

v2-8b6082ae13a579cc0bb38d679aacfbad_720w.png

3.HashMap为什么是线程不安全的

  • 在jdk1.7中,在多线程的环境下,扩容时会出现死循环,数据丢失 的问题
  • 在jdk1.8+中,在多线程的环境下,会发生数据覆盖的问题

原因:

  • 在jdk1.7中,HashMap扩容时使用的是头插法插入元素 。具体原因:在HashMap出发扩容时,正好两个线程同时在操作同一个链表,当线程A被挂起,线程B完成数据插入,等cpu资源释放,线程A重新执行之前的逻辑,数据已经发生改变,线程A,B,数据会形成环形链表造成死循环,数据丢失问题

  • 在jdk1.8中,HashMap扩容使用了尾插法 这样避免了死循环问题,由于多线程对HashMap进行put操作,调用了HashMap#putVal(),如果两个线程并发执行 put 操作,并且两个数据的 hash 值冲突,就可能出现数据覆盖。具体原因:线程 A 判断 hash 值位置为 null,还未写入数据、由于时间片耗尽导致被挂起,此时线程 B 正常插入数据。接着线程 A 获得时间片,由于线程 A 之前已进行hash碰撞的判断,所以此时不会再进行判断、而是直接进行插入,就会把刚才线程 B 写入的数据覆盖掉

jdk1.7扩容代码如下

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);e.next = newTable[i];newTable[i] = e;e = next;}}}

在多线程下安全使用HashMap,可以使用一下策略

  1. 使用线程安全替代类 :ConcurrentHashMap集合类,强烈推荐
  2. 使用线程局部变量 : 为每个线程维护一个独立的HashMap实例,以避免线程间竞争。ThreadLocal<Map<String, Integer>> threadLocalMap = ThreadLocal.withInitial(HashMap::new);

4.HashMap和ConcurrentHashMap区别

  • 线程是否安全

  • HashMap不是线程安全的

  • concurrentHashMap是线程安全的,是通过segment分段锁-继承ReentrantLock(JDK1.7可重入锁),cas和synchronized(jdk1.8内置锁)来进行加锁,实现线程安全

  • 底层数据结构

  • HashMap:在jdk1.7时,数组+链表,jdk1.8时采用数组+链表+红黑树

  • ConcurrentHashMap:JDK1.8之前Segment+数组+链表,JDK1.8之后数组+链表+红黑树

5.HashMap和HashTable区别

Hashtable和HashMap都是 基于hash表实现的K-V结构的集合,Hashtable是jdk1.0引入的一个线程安全的集合类,内部使用数组+链表的形式来实现

从功能特性的角度来说

1、Hashtable是线程安全的(HashTable 对每个方法都增加了 synchronized),而HashMap不是

2、HashMap的性能要比Hashtable更好,因为Hashtable采用了全局同步锁来保证安全性,对性能影响较大

从内部实现的角度来说

1)Hashtable使用数组加链表,HashMap JDK1.7数组+链表、JDK1.8 数组+链表+红黑树

2)HashMap初始容量是16,Hashtable初始容量是11

3)HashMap可以使用null作为key;而Hashtable不允许 null 作为 Key,会抛出NullPointerException异常

他们两个的key的散列算法不同:Hashtable直接是使用key的hashcode对数组长度取模;而HashMap对key的hashcode做了二次散列,从而避免key的分布不均匀影响到查询性能

6.HashMap、Hashtable、ConcurrentHashMap区别

HashMap、Hashtable、ConcurrentHashMap都是 基于hash表实现的K-V结构的集合,在线程安全、底层数据结构方面有所区别

  • HashMap:线程不安全,因为HashMap中操作都没有加锁,因此在多线程环境下会导致数据覆盖之类的问题,所以,在多线程中使用HashMap是会抛出异常的
  • Hashtable:线程安全,但是Hashtable只是单纯的在添加put、删除remove、查询get方法上加synchronized,保证插入时阻塞其他线程的插入操作。虽然安全,但因为设计简单,所以性能低下(HashMap的性能要比Hashtable更好,因为Hashtable采用了全局同步锁来保证安全性,对性能影响较大)
  • ConcurrentHashMap:线程安全,ConcurrentHashMap并非锁住整个方法,而是通过原子操作和局部加锁的方法保证了多线程的线程安全,且尽可能减少了性能损耗。Segment分段锁–继承 ReentrantLock(JDK1.7重入锁)、CAS和synchronized(JDK1.8内置锁)

7.为什么 HashMap 采用拉链法而不是开放地址法?

Java 给予 HashMap 的定位是一个相对通用的散列表容器,它应该在面对各种输入的时候都表现稳定。而开发地址法相对来说容易出现数据堆积,在数据量较大时可能出现连续冲突的情况,性能不够稳定。

我们可以举个反例,在 Java 原生的数据结构中,也存在使用开放地址法的散列表 —— 就是 ThreadlLocal。因为项目中不会大量使用 ThreadLocal 线程局部存储,所以它是一个小规模数据场景,这里使用开发地址法是没问题的。

8.Map对比

实现类数据结构是否线程安全key是否可为null是否有序
HashMap哈希表结构,jdk1.7 数组+链表,jdk1.8 数组+链表+红黑树
ConcurrentHashMap哈希表结构,jdk1.7 数组+链表,jdk1.8 数组+链表+红黑树
Hashtable哈希表结构,数组+链表
LinkedHashMap继承自HashMap,数组+链表+红黑树+双重链接列表
TreeMap红黑树

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/31157.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SSM+Jsp的水果销售管理网站

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

Linux 五种IO模型

注&#xff1a;还有一种信号驱动IO&#xff0c;使用较少暂不讨论&#xff1b; 一&#xff0c;区分阻塞、非阻塞和同步、异步 看了很多文章对这两组概念解释和对比&#xff0c;说的太复杂了&#xff0c;其实没必要&#xff0c;两句话就能说清楚。 首先&#xff0c;对于读数据rec…

探索监管沙箱在金融科技行业中的应用

一、引言 随着金融科技的快速发展&#xff0c;传统金融机构与科技企业之间的竞争也日趋激烈。为了平衡金融科技创新与风险防控&#xff0c;各国监管机构纷纷引入监管沙箱&#xff08;Regulatory Sandbox&#xff09;机制。监管沙箱作为一个受监督的安全测试区&#xff0c;允许金…

Linux字节对齐小程序

#include <stdio.h> // 默认对齐 struct DefaultAligned { char c; int i; }; // 按1字节对齐 #pragma pack(push, 1) struct OneByteAligned { char c; int i; }; #pragma pack(pop) // 恢复之前的对齐设置 int mai…

Python日志管理利器:如何高效管理平台日志

一、为什么需要日志管理&#xff1f; 日志是应用程序的重要组成部分&#xff0c;它记录了应用程序的运行状态、错误信息以及用户交互等关键信息。良好的日志管理可以帮助开发人员及时发现和解决问题&#xff0c;提高应用程序的稳定性和可靠性。 项目在本地开发调试时&#xf…

基于Sringboot+Vue的校园招聘系统【原创】【开源】

浏览器&#xff1a;Chrome或360浏览器 系统环境配置 前置条件&#xff1a;系统已经安装了Mysql5.7、Mysql工具&#xff08;Navicat&#xff09;、JDK1.8、Maven3.6.1、vue3.0以下开发环境、 Intellij Idea、 Chrome或360浏览器 1、导入数据库 2、编译前端代码vue 编译&…

HTML播放flv

页面效果&#xff1a; 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" …

森林火灾扑救特类车辆有哪些_鼎跃安全

森林消防是在森林火灾发生时&#xff0c;为了保护森林资源&#xff0c;防止火势蔓延&#xff0c;采取了一系列的应用措施&#xff0c;针对自然环境中的火灾消防工作。森林灭火主要包括预警、预防措施、火情监测、火势控制和灭火等&#xff0c;森林火灾发生的地形往往复杂崎岖&a…

【银河麒麟】高可用触发服务器异常重启,处理机制详解

1.服务器环境以及配置 【机型】物理机 处理器&#xff1a; Intel 内存&#xff1a; 126G 【内核版本】 4.19.90-25.16.v2101.ky10.x86_64 【银河麒麟操作系统镜像版本】 Kylin-Server-10-SP2-Release-Shenzhen-Metro-x86-Build01-20220619 Kylin-HA-10-SP2-Release-S…

yolov10打包为exe

一、前言 本节实验将官方yolov10推理程序打包为exe运行 二、代码 首先下载官方代码至本机&#xff0c;并使用conda创建虚拟环境&#xff0c;并安装好yolov10所需库 conda create --prefix E:/pyenv/myYolo10 python3.8 pip install -r requirements.txt 下载官方模型权重 …

深入理解和实现Windows进程间通信(消息队列)

常见的进程间通信方法 常见的进程间通信方法有&#xff1a; 管道&#xff08;Pipe&#xff09;消息队列共享内存信号量套接字 下面&#xff0c;我们将详细介绍消息队列的原理以及具体实现。 什么是消息队列&#xff1f; Windows操作系统使用消息机制来促进应用程序与操作系…

Git 查看当前分支是基于哪个分支拉取(源头分支)

场景&#xff1a; 项目中使用 Git 管理代码仓库的时候&#xff0c;随着项目的持续迭代及项目的扩展&#xff0c;多版本并行开发是非常常见的事情&#xff0c;多版本并行开发就伴随着多分支&#xff0c;随着 Git 的分支越拉越多&#xff0c;这时候很容易造成分支的混乱&#xf…

蓝牙模块在车载系统中的应用与集成:现状、挑战与未来展望

随着科技的快速发展&#xff0c;蓝牙技术已经深入到我们生活的方方面面&#xff0c;其中车载系统中的应用尤为显著。蓝牙模块作为一种无线通信技术&#xff0c;不仅为驾驶者提供了更加便捷的操作体验&#xff0c;同时也提升了驾驶的安全性。本文旨在分析蓝牙模块在车载系统中的…

selenium框架学习

概念 WEB自动化框架 三大组件: selenium IDE 浏览器插件,实现脚本录制WebDriver 实现对浏览器的各种操作(API包)Grid 实现同时对多个用例进行执行,用例在多个浏览器同步执行环境搭建 1、安装selenium: pip install selenium2、安装浏览器 3、安装浏览器驱动(对应的驱…

东郊到家类型小程序APP软件基于SpringBoot开发的系统源码

项目背景 在快节奏的现代生活中&#xff0c;人们越来越追求高效、便捷的生活方式。上门服务作为一种新型的服务模式&#xff0c;正逐渐受到广大用户的青睐。而这一切的背后&#xff0c;离不开技术的强大支撑。今天&#xff0c;我们就来探讨一下上门服务类型软件的技术魅力&…

React Native性能优化红宝书

一、React Native介绍 React Native 是Facebook在React.js Conf2015 推出的开源框架&#xff0c;使用React和应用平台的原生功能来构建 Android 和 iOS 应用。通过 React Native&#xff0c;可以使用 JavaScript 来访问移动平台的 API&#xff0c;使用 React 组件来描述 UI 的…

WebHttpServletRequestResponse(完整知识点汇总)

额外知识点 Web核心 Web 全球广域网&#xff0c;也成为万维网&#xff08;www&#xff09;&#xff0c;可通过浏览器访问的网站 JavaWeb 使用Java技术来解决相关Web互联网领域的技术栈 JavaWeb技术栈 B/S架构&#xff1a;Browser/Server&#xff0c;即浏览器/服务器 架构模式…

ROS2 概念以及通信方式

一、ros2的相关概念 ROS 2&#xff08;Robot Operating System 2&#xff09;是一个用于机器人开发的开源平台&#xff0c;它提供了一系列工具和库&#xff0c;用于构建机器人应用程序。相较于前身ROS&#xff08;ROS 1&#xff09;&#xff0c;ROS 2在设计上考虑了更多的实时…

IT入门知识第六部分《后端开发》(6/10)

目录 后端开发&#xff1a;构建强大的服务端逻辑 1.引言 2.后端技术概述 2.1 数据库 2.2 服务器 2.3 API 2.4 安全性 3.服务器和API 3.1 服务器的作用 3.2 API的作用 4.后端框架 4.1 Node.js 4.1.1 特点 4.1.2 用途 4.1.3 代码案例分析 4.2 Django 4.2.1 特点 …

mac赛车竞速游戏:弯道卡丁车车手 for Mac 中文版下载

《弯道卡丁车车手》是一款刺激的卡丁车竞速游戏&#xff0c;玩家扮演的是赛道上的卡丁车车手&#xff0c;需要在曲线崎岖的赛道上驾驶卡丁车&#xff0c;与其他车手展开激烈的竞速比赛。 游戏中有多种赛道可以选择&#xff0c;每个赛道都有不同的难度和特点&#xff0c;玩家需…