go基础——map/sync.Map

注:权作为学习笔记,基于version: 1.14

内容

一 map
1.1 数据结构
1.2 写入
1.3 查找
1.4 扩容
1.5 注意点

二 sync.map
2.1 基本使用
2.2 实现原理

一 map

1.1 数据结构

学习go中map的数据结构,可以对比着java中的hashmap实现来一起对比学习,java 中map是采用拉链罚来解决hash冲突,基本数据结构是数组+链表的hash表形式;而go map并没有采取这种形式,go中采取buckets的形式,每个bucket在固定了可以存储8对键/值,总体数据结构大体如下图这个样子:


image.png

源码长这个样子:

// src/runtime/map.go
type hmap struct {
    count        int               /* Map中元素数量 */
    flags        int8              /* 相关标志位 */
    B            int8              /* (1<< B * 6.5)为最多元素的数量 ; 代表bucket的个数,不是直接B个桶,是当前map有2^B个桶;*/
    noverflow    int16             /* 溢出桶的数量 */
    hash0        uint32            /* 哈希种子 */
    buckets      unsafe.Pointer    /* 桶指针 */
    oldbuckets   unsafe.Pointer    /* 桶指针(只有扩容的时候才使用,指向旧的桶) */
    nevacuate    uintptr           /* 用于桶搬迁 */
    extra        *mapextra         /* 当key/value非指针时使用 */
}

type mapextra struct {
    overflow      *[]*bmap         /* 溢出桶的指针 */
    oldoverflow   *[]*bmap             
    nextOverflow  *bmap                   
}

type bmap struct {
    tophash  [bucketCnt]uint8      /* bucketCnt=8,一个桶只能存放8对k/v, 低B位用来寻找桶,高8位用来寻找元素 */
}

/* 当kev/value不为指针时,溢出桶存放到mapextra结构中,overflow存放buckets中的溢出
    桶,oldoverflow存放oldbuckets中的溢出桶,nextOverflow预分配溢出桶空间 */
type mapextra struct {
    overflow        *[]*bmap       /* 以切片形式存放buckets中的每个溢出桶 */
    oldoverflow     *[]*bmap       /*  以切片形式存放oldbuckets中的每个溢出桶*/
    nextOverflow    *bmap
}

/* map标志位 */
iterator           1        /* 迭代器buckets桶的标志位,为1表示正在使用buckets*/
oldIterator        2        /* 迭代器oldbuckets桶的标志位 ,为1表示正在使用oldbuckets*/
hashWriting        3        /* 并发写标志位,为1表示有goroutine正在写map */
sameSizeGrow       4        /* 等量扩容标志,表示申请的桶数量和原来一样 */

总体来说:

  • go的map使用2^B个桶来装数据,每个桶固定存储8对键值对,每个bucket是一个bmap类型,bmap 结构体里有个字段tophash,是一个数组,这个数组存放了bmap里各个key的hash的前8位,用来快速对比key是否相等,如果一个key的前8位都不相等,就没有必要再往下对比了;
  • 除了buckets, hmap还要个关键的东西,就是overflow,这个是一个bmap类型的指针数组,存放的是指向溢出桶的指针;溢出桶是用来存放,当一个key hash 后,用key hash的后B位计算出在那个桶,然后遍历桶是否有空位置,如果当前桶没有空位置了,那么会在当前桶后面连续的内存位置开辟一个新 bucket来存放,这个新的bucket就是溢出桶
    类似这样的结构:


    image.png

1.2 写入

put的大体步骤是:

  • 1 根据key 计算hash值
  • 2 根据B的值,得到key hash值的后B位,这个就是会存于那个桶
  • 3 遍历桶,找到一个空位置,然后存入到空位置;
  • 4 如果当前桶全满,如果有溢出桶,则遍历溢出桶,存到溢出桶的空位置;如果没有溢出桶则新建一个溢出桶存放key

1.3 查找

查找和写入步骤很类似,查找时会利用 bmap的tophash快速对比,如果key的前8位和和tophash里的值都不一样,那证明不在当前桶,可能在溢出桶里,加快遍历速度

1.4 扩容

当满足一下任何一个条件时,会发生扩容:

  • 1 装载因子>6.5时,hmap中元素太多了;其中,装载因子=count/2^B
  • 2 hmap中溢出桶太多了,当 B 小于 15,也就是 bucket 总数 2^B 小于 2^15 时,如果 overflow 的 bucket 数量超过 2^B;当 B >= 15,也就是 bucket 总数 2^B 大于等于 2^15,如果 overflow 的 bucket 数量超过 2^15。
    扩容方式:
  • 1 溢出桶太多,即上述条件2,此时是 等量扩容, 即整理桶中个数据,将分散的数据进行搬移整理到一块;
  • 2 当装载因子>6.6, 增量扩容(2倍扩容);

1.5 注意点

  • 1 go map不是并发安全的
  • 2 如果只是声明了map, 而没有初始化,是不能直接添加值的; 要区别slice, 如果声明一个slice, 是直接可以使用append添加元素的;
var map1 map[string]int
map1["ss"] = 1 //  直接panic, map1是nil

二 sync.map

map是非线程安全的,在go中如果想用线程安全的map, 一般有两种方式:

  • 1 自己构造一个struct, struct里放一个RWMutex, 读时加读锁,写时加写锁, 类似这样的:
type SyncMap struct {
    sync.RWMutex
    m map[string]string
}
  • 2 sync包下面的sync.map

2.1 基本使用

func main(){
    var sm sync.Map
    // 写入
    sm.Store("a", 1)
    sm.Store("b", 2)
    sm.Store("c", 3)

    // 取出
    v, _ := sm.Load("a")
    fmt.Println(v.(int))

    // 删除
    sm.Delete("b")

    // 存在则获取 不存在则添加
    sm.LoadOrStore("d", 4)

    // 遍历
    sm.Range(func(key, value interface{}) bool {
        fmt.Println(key.(string), value.(int))
        return true
    })
}

2.2 基本原理

基本数据结构如下:

type Map struct {
    mu Mutex
    read atomic.Value // readOnly
    dirty map[interface{}]*entry
    misses int
}

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}
  • 关键的字段是:read和dirty, read属性是atomic.Value(原子的更新复杂类型,可以看做一个容器),本质存储的是readOnly 结构体,可见read和dirty结构体都是一个简单的map,
  • 通过一种空间换时间,通过双份的map来实现读不加锁,写加锁的思路进行优化
  • read和dirty的map的value都是指向同一份的指针,所以双份的map的存储在空间上也做了很好的优化
  • sync.map适合并发读多写少的场景,如果并发写远远大于并发读,第一种加读写锁的并发模式的速度要更好一些

详细理解:

引用:
1 Golang之Map源码解析:https://www.jianshu.com/p/17f49e562f7b
2 大话图解golang map https://studygolang.com/articles/21047?fr=sidebar
3 《go 语言设计与实现》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353