581 字
3 分钟
Go中的Map
底层原理
Go Map 的底层实现是一个哈希表,在运行时表现为一个指向 hmap 结构体的指针,hamp 中记录了桶数组指针 buckets、溢出桶指针以及元素个数等字段。每个桶是一个 bmap 结构体,能储存8个键值对和8个 tophash,并有指向下一个溢出桶的指针 overflow。为了内存紧凑,bmap 中采用的是先存8个键再存8个值的存储方式。
Map是否是并发安全的?
map 不是线程安全的。 在查找、赋值、遍历、删除的过程中都会检测写标志,一旦发现写标志置位(等于1),则直接 panic。赋值和删除函数在检测完写标志是复位之后,先将写标志位置位,才会进行之后的操作。
扩容机制
向 map 插入新 key 的时候,会进行条件检测,符合下面这2个条件,就会触发扩容。
- 装载因子超过阈值,源码里定义的阈值是6.5,这个时候会触发双倍扩容
- overflow 的 bucket 数量过多时这两种情况触发等量扩容:
- 当 ,也就是 bucket 总数 小于 时,如果 overflow 的bucket 数量 超过
- 当 ,也就是 bucket 总数 大于等于 时,如果 overflow 的bucket 数量 超过
Go 的扩容是渐进式(Gradual)的。它不会在触发扩容时“stop the world”来一次性吧所有数据搬迁岛新空间,而是只分配新空间,然后在后续的每一次插入、修改或删除操作时,才会顺便搬迁一两个旧桶的数据。这种设计将庞大的扩容成本分摊到了多次操作中,极大地减少了服务的瞬间延迟(STW),保证了性能的平滑性。
如果是触发双倍扩容,会新建一个 buckets 的数组,新的 buckets数量大小是原来的两倍,然后旧 buckets 数据搬迁到新的 buckets。如果是等量扩容,buckets 数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 buckets 中的 key 排列地更紧密,这样节省空间,存取效率更高。