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,否则会溢出、失真,所以它改成了抽样计数。






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

相关推荐
程序员清风3 分钟前
北京回长沙了,简单谈谈感受!
java·后端·面试
何中应12 分钟前
请求头设置没有生效
java·后端
NPE~15 分钟前
自动化工具Drissonpage 保姆级教程(含xpath语法)
运维·后端·爬虫·自动化·网络爬虫·xpath·浏览器自动化
凡人叶枫26 分钟前
C++中输入、输出和文件操作详解(Linux实战版)| 从基础到项目落地,避坑指南
linux·服务器·c语言·开发语言·c++
zheyutao33 分钟前
字符串哈希
算法
春日见38 分钟前
车辆动力学:前后轮车轴
java·开发语言·驱动开发·docker·计算机外设
A尘埃42 分钟前
保险公司车险理赔欺诈检测(随机森林)
算法·随机森林·机器学习
锐意无限42 分钟前
Swift 扩展归纳--- UIView
开发语言·ios·swift
低代码布道师42 分钟前
Next.js 16 全栈实战(一):从零打造“教培管家”系统——环境与脚手架搭建
开发语言·javascript·ecmascript
宋小黑1 小时前
JDK 6到25 全版本网盘合集 (Windows + Mac + Linux)
java·后端