Golang原理剖析(map面试与分析)

文章目录

  • [1. map的底层实现原理](#1. map的底层实现原理)
  • [2. 为什么遍历map是无序的?](#2. 为什么遍历map是无序的?)
  • [3. 如何实现有序遍历map?](#3. 如何实现有序遍历map?)
  • [4. 为什么Go map是非线程安全的?](#4. 为什么Go map是非线程安全的?)
  • [5. 线程安全的map如何实现?](#5. 线程安全的map如何实现?)
  • [6. Go sync.map 和原生 map 谁的性能好,为什么?](#6. Go sync.map 和原生 map 谁的性能好,为什么?)
  • [7. 为什么 Go map 的负载因子是 6.5?](#7. 为什么 Go map 的负载因子是 6.5?)
  • [8. map扩容策略是什么?](#8. map扩容策略是什么?)

1. map的底层实现原理

Map的底层实现数据结构实际上是一个哈希表 。在运行时表现为个指向hmap结构的指针,hmap中有记录了桶数组指针溢出桶指针以及元素个数字段每个桶是一个bmap的数据结构,可以存储8个键值对和8个tophash以及指向下一个溢出桶的指针为了内存紧凑,采用的是先存8个key过后再存value。

2. 为什么遍历map是无序的?

map在遍历的时候会随机一个桶号和槽位,从这个随机的桶号开始,在每个桶中从这个随机的槽位开始遍历完所有的桶。至于为什么要随机开始?

因为map 在扩容后,会发生 key 的搬迁,原来落在同一个 bucket 中的 key,搬迁后,有些 key 的位置就会发生改变。而遍历的过程,就是按顺序遍历 bucket,同时按顺序遍历 bucket 中的 key。搬迁后,key 的位置发生了重大的变化,有些 key 飞上高枝,有些 key 则原地不动。这样,遍历 map 的结果就不可能按原来的顺序了。所以,go语言,强制每次遍历都随机开始。

就是让使用map的程序员都知道,遍历时无序的 ​ 如果不这么设计的话,没扩容之前时一直一个顺序 ​ 但扩容之后顺序就全然变化了,那会让使用者面临更多困惑 ​ 所以不如每一次都时随机遍历

3. 如何实现有序遍历map?

可以在遍历的时候将结果保存到一个slice里面,对slice进行排序

4. 为什么Go map是非线程安全的?

go官方给出的原因是:map 适配的场景应该是简单的(不需要从多个 goroutine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持

在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 fatal error。赋值和删除函数在检测完写标志是复位之后,先将写标志位置位,才会进行之后的操作。

检测写标志:

go 复制代码
if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }

设置写标志:

go 复制代码
h.flags |= hashWriting

5. 线程安全的map如何实现?

加锁或者使用sync.map

6. Go sync.map 和原生 map 谁的性能好,为什么?

原生map的性能好,因为Go sync.map为了保证线程安全,以空间换时间,采用了read和dirty两个 map ,用了原子操作和锁两种方式来实现线程安全,只是在操作read的时候用原子操作,当read中不存在某个key的时候要加锁操作dirty,过程中还是会有加锁操作,所以性能上会有损耗。

7. 为什么 Go map 的负载因子是 6.5?

%overflow = 发生过溢出的主 bucket 数 / 主 bucket 总数

注意:它不是"溢出桶数量占比",而是"有 overflow 的主桶占比"。

一个主桶只要挂了至少一个 overflow bucket,就计入。

overflow 链越长,cache locality 越差(内存访问更散)




bash 复制代码
负载因子 = 哈希表存储的元素个数 / 桶个数

装载因子越大,填入的元素越多,空间利用率就越高,但发生哈希冲突的几率就变大。

装载因子越小,填入的元素越少,冲突发生的几率减少,但空间浪费也会变得更多,而且还会提高扩容操作的次数

源码里对负载因子的定义是6.5,是经过测试后取出的一个比较合理的值,

每个 bucket 有 8 个空位,假设map里所有的数组桶都装满元素,没有一个数组桶有溢出桶,那么这时的负载因子刚好是8。而负载因子是6.5的时候,说明数组桶快要用完了,存在溢出的情况下,查找一个key很可能要去遍历溢出桶,会造成查找性能下降,所以有必要扩容了

8. map扩容策略是什么?

负载因子已经超过6.5:双倍扩容

溢出桶的数量过多:等量扩容(一般认为溢出桶数量接近正常桶数量时)

当溢出桶过多,发生等量扩容,溢出桶过多的标志:

  1. 当正常桶数量小于215的时候,溢出桶多于正常桶,溢出桶数量就过多了
  2. 当正常桶数量大于215 的时候,溢出桶一旦多于215,溢出桶数量就过多了



两个条件之一

noverflow >= 2B

noverflow >= 215

noverflow是溢出桶数量

incrnoverflow() 是 overflow 桶计数器:map 小时精确数,map 大时为了不让 16 位计数器溢出,就改用"随机抽样"来近似计数;当这个近似计数达到约 215 时,就说明 overflow 已经多到该触发等量扩容了。

通俗点讲:incrnoverflow() 就是在"记溢出桶有多少个",用来判断要不要触发等量扩容。

但因为计数器 noverflow 太小(只有 uint16,最多 65535),当 map 很大时不能每个溢出桶都加 1,否则会溢出、失真,所以它改成了抽样计数。






之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

相关推荐
WenJGo2 小时前
基于 IPIDEA 的 GitHub 代码文件抓取与数据可视化实践(Python 实现)
后端
Kingairy2 小时前
Python面试高频题
java·python·面试
CodeByV2 小时前
【算法题】栈
算法
黎雁·泠崖2 小时前
Java数组入门:定义+静态/动态初始化全解析(隐式转换+案例+避坑指南)
java·开发语言·python
m0_748252382 小时前
JavaScript 基本语法
开发语言·javascript·ecmascript
froginwe112 小时前
传输对象模式(Object Transfer Pattern)
开发语言
sprintzer2 小时前
1.6-1.15力扣数学刷题
算法·leetcode·职场和发展
上进小菜猪2 小时前
基于 YOLOv8 的高压输电线路(绝缘子、电缆)故障自动识别 [目标检测完整源码]
后端
qq_406176142 小时前
深入理解 JavaScript 闭包:从原理到实战避坑
开发语言·前端·javascript