基础筑基

在大多数语言中原始map都不是一个线程安全的数据结构,那如果要在多个线程或者goroutine中对线程进行更改就需要加锁,除了加1个大锁,不同的语言还有不同的优化方式, 像在java和go这种语言其实都采用的是链表法来进行map的实现,本文也主要分析这种场景

并发安全的map实现的三种方式

在go语言中实现多个goroutine并发安全访问修改的map的方式,主要有如下三种:

实现方式 原理 适用场景
map+Mutex 通过Mutex互斥锁来实现多个goroutine对map的串行化访问 读写都需要通过Mutex加锁和释放锁,适用于读写比接近的场景
map+RWMutex 通过RWMutex来实现对map的读写进行读写锁分离加锁,从而实现读的并发性能提高 同Mutex相比适用于读多写少的场景
sync.Map 底层通分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化 读多修改少,元素增加删除频率不高的情况,在大多数情况下替代上述两种实现

上面三种实现具体的性能差异可能还要针对不同的具体的业务场景和平台、数据量等因此来进行综合的测试,源码的学习更多的是了解其实现细节,以便在出现性能瓶颈的时候可以进行分析,找出解决解决方案

map的容量问题



在Mutex和RWMutex实现的并发安全的map中map随着时间和元素数量的增加、删除,容量会不断的递增,在某些情况下比如在某个时间点频繁的进行大量数据的增加,然后又大量的删除,其map的容量并不会随着元素的删除而缩小,而在sync.Map中,当进行元素从dirty进行提升到read map的时候会进行重建,可能会缩容

无锁读与读写分离

读写分离

并发访问map读的主要问题其实是在扩容的时候,可能会导致元素被hash到其他的地址,那如果我的读的map不会进行扩容操作,就可以进行并发安全的访问了,而sync.map里面正是采用了这种方式,对增加元素通过dirty来进行保存

无锁读

通过read只读和dirty写map将操作分离,其实就只需要通过原子指令对read map来进行读操作而不需要加锁了,从而提高读的性能

写加锁与延迟提升

写加锁

上面提到增加元素操作可能会先增加大dirty写map中,那针对多个goroutine同时写,其实就需要进行Mutex加锁了

延迟提升

上面提到了read只读map和dirty写map, 那就会有个问题,默认增加元素都放在dirty中,那后续访问新的元素如果都通过 mutex加锁,那read只读map就失去意义,sync.Map中采用一直延迟提升的策略,进行批量将当前map中的所有元素都提升到read只读map中从而为后续的读访问提供无锁支持

指针与惰性删除

map里面的指针

map里面存储数据都会涉及到一个问题就是存储值还是指针,存储值可以让 map作为一个大的的对象,减轻垃圾回收的压力(避免扫描所有小对象),而存储指针可以减少内存利用,而sync.Map中其实采用了指针结合惰性删除的方式,来进行 map的value的存储

惰性删除

惰性删除是并发设计中一中常见的设计,比如删除某个个链表元素,如果要删除则需要修改前后元素的指针,而采用惰性删除,则通常只需要给某个标志位设定为删除,然后在后续修改中再进行操作,sync.Map中也采用这种方式,通过给指针指向某个标识删除的指针,从而实现惰性删除

源码分析

数据结构分析

Map

type Map struct {
mu Mutex
// read是一个readOnly的指针,里面包含了一个map结构,就是我们说的只读map对该map的元素的访问
// 不需要加锁,只需要通过atomic加载最新的指针即可
read atomic.Value // readOnly // dirty包含部分map的键值对,如果要访问需要进行mutex获取
// 最终dirty中的元素会被全部提升到read里面的map中
dirty map[interface{}]*entry // misses是一个计数器用于记录从read中没有加载到数据
// 尝试从dirty中进行获取的次数,从而决定将数据从dirty迁移到read的时机
misses int
}

readOnly

只读map,对该map元素的访问不需要加锁,但是该map也不会进行元素的增加,元素会被先添加到dirty中然后后续再转移到read只读map中,通过atomic原子操作不需要进行锁操作

type readOnly struct {
// m包含所有只读数据,不会进行任何的数据增加和删除操作
// 但是可以修改entry的指针因为这个不会导致map的元素移动
m map[interface{}]*entry
// 标志位,如果为true则表明当前read只读map的数据不完整,dirty map中包含部分数据
amended bool
}

entry

entry是sync.Map中值得指针,如果当p指针指向expunged这个指针的时候,则表明该元素被删除,但不会立即从map中删除,如果在未删除之前又重新赋值则会重用该元素

type entry struct {
// 指向元素实际值得指针
p unsafe.Pointer // *interface{}
}

数据的存储

2.2.1 元素存在只读map

元素如果存储在只读map中,则只需要获取entry元素,然后修改其p的指针指向新的元素就可以了,因为是原地操作所以map不会发生变化

    read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}

元素存在进行变更后的只读map中

如果此时发现元素存在只读 map中,则证明之前有操作触发了从dirty到read map的迁移,如果此时发现存在则修改指针即可

    read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
// 如果key之前已经被删除,则这个地方会将key从进行nil覆盖之前已经删除的指针
// 然后将它加入到dirty中
m.dirty[key] = e
}
// 调用atomic进行value存储
e.storeLocked(&value)
}

元素存在dirty map中

如果元素存在dirty中其实同read map逻辑一样,只需要修改对应元素的指针即可

    } else if e, ok := m.dirty[key]; ok {
// 如果已经在dirty中就会直接存储
e.storeLocked(&value)
} else {

元素之前不存在

如果元素之前不存在当前Map中则需要先将其存储在dirty map中,同时将amended标识为true,即当前read中的数据不全,有一部分数据存储在dirty中

        // 如果当前不是在修正状态
if !read.amended {
// 新加入的key会先被添加到dirty map中, 并进行read标记为不完整
// 如果dirty为空则将read中的所有没有被删除的数据都迁移到dirty中
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)

数据迁移

read map到dirty map的迁移



在刚初始化和将所有元素迁移到read中后,dirty默认都是nil元素,而此时如果有新的元素增加,则需要先将read map中的所有未删除数据先迁移到dirty中

func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
} read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}

dirty到read map的迁移



当持续的从read访问穿透到dirty中后,就会触发一次从dirty到read的迁移,这也意味着如果我们的元素读写比差比较小,其实就会导致频繁的迁移操作,性能其实可能并不如rwmutex等实现

func (m *Map) missLocked() {
m.misses++
if m.misses < len(m.dirty) {
return
}
m.read.Store(readOnly{m: m.dirty})
m.dirty = nil
m.misses = 0
}

数据加载

只读无锁

Load数据的时候回先从read中获取,如果此时发现元素,则直接返回即可

   read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]

加锁读取read和dirty

加锁后会尝试从read和dirty中读取,同时进行misses计数器的递增,如果满足迁移条件则会进行数据迁移

       read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// 这里将采取缓慢迁移的策略
// 只有当misses计数==len(m.dirty)的时候,才会将dirty里面的数据全部晋升到read中
m.missLocked()
}

数据删除



数据删除则分为两个过程,如果数据在read中,则就直接修改entry的标志位指向删除的指针即可,如果当前read中数据不全,则需要进行dirty里面的元素删除尝试,如果存在就直接从dirty中删除即可

func (m *Map) Delete(key interface{}) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
delete(m.dirty, key)
}
m.mu.Unlock()
}
if ok {
e.delete()
}
}

mutex与加锁后的read map重复读

因为mutex互斥的是所有操作,包括dirty map的修改、数据的迁移、删除,如果在进行m.lock的时候,已经有一个提升dirty到read操作在进行,则执行完成后dirty实际上是没有数据的,所以此时要再次进行read的重复读

微信号:baxiaoshi2020

关注公告号阅读更多源码分析文章

更多文章关注 www.sreguide.com

本文由博客一文多发平台 OpenWrite 发布!

图解Go里面的sync.Map了解编程语言核心实现源码的更多相关文章

  1. 图解Go语言的context了解编程语言核心实现源码

    基础筑基 基于线程的编程语言中的一些设计 ThreadGroup ThreadGroup是基于线程并发的编程语言中常用的一个概念,当一个线程派生出一个子线程后通常会加入父线程的线程组(未指定线程组的情 ...

  2. 图解Go里面的互斥锁mutex了解编程语言核心实现源码

    1. 锁的基础概念 1.1 CAS与轮询 1.1.1 cas实现锁 在锁的实现中现在越来越多的采用CAS来进行,通过利用处理器的CAS指令来实现对给定变量的值交换来进行锁的获取 1.1.2 轮询锁 在 ...

  3. apache_commons 之 双向Map DualHashBidiMap (使用及源码)

    在项目当中,经常出现需要根据Key值获取value:而且要求根据value获取key值,其实在commons-collections包中已经提供了此集合类.就是DualHashBidiMap类. (官 ...

  4. Scala实战高手****第8课:零基础实战Scala最常用数据结构Map和Tuple及Spark源码鉴赏

    本课内容1.Map和Tuple在Spark源码中的鉴赏2.Map和Tuple代码操作实战 ------------------------------------------------------- ...

  5. Java遍历时删除List、Set、Map中的元素(源码分析)

    在对List.Set.Map执行遍历删除或添加等改变集合个数的操作时,不能使用普通的while.for循环或增强for.会抛出ConcurrentModificationException异常或者没有 ...

  6. 集合框架-Map集合-LinkedHashMap及关联源码操作

    1 package cn.itcast.p9.linkedhashmap.demo; 2 3 import java.util.HashMap; 4 import java.util.Iterator ...

  7. 帮助你实现漂亮界面的14套免费的 HTML/CSS 源码

    在网络上能找很多免费的 PSD 格式素材,但是很少有 HTML/CSS 界面组件下载.在这篇文章中,收集了14套免费的 HTML/CSS 界面源码分享给前端设计师和开发者们.这些组件包括按钮.滑块.表 ...

  8. Spring里bean之间的循环依赖解决与源码解读

    通过前几节的分析,已经成功将bean实例化,但是大家一定要将bean的实例化和完成bean的创建区分开,bean的实例化仅仅是获得了bean的实例,该bean仍在继续创建之中,之后在该bean实例的基 ...

  9. 并发安全 sync.Map

    https://mp.weixin.qq.com/s/MqPm7QH3_D9roVkpTs9Xpw 谈谈Go的并发安全相关 原创 歪鼻子 歪鼻子 2020-12-27     package main ...

随机推荐

  1. thinkphp 公用函数

    thinkphp 的项目中指定了common/function.php的函数是公用函数. 不但php页面可以使用,模板文件也可以使用:

  2. 17.使用android_studio开发libgdx

    以前用eclipse开发libgdx,由于每次开机都会自检一遍安卓环境,觉得慢,就把安卓项目包给关掉了,结果再打开资源目录发生了变化,导致安卓打包不了,所以决定尝试使用as开发 首先安装as,导入gd ...

  3. 《C语言深度解剖》学习笔记之符号

    第2章 符号 1.注释符号 编译器会将注释剔除,用空格代替原来的注释 y=x /* p; 编译器提示出错的原因:实际上,编译器会把“/*”当作一段注释的开始,直到出现“*/”为止. [规则 2-1]注 ...

  4. 阿里云POLARDB荣膺2019中国数据库年度最佳创新产品

    在日前的DTCC 2019(第十届中国数据库技术大会)上,阿里云自研云原生数据库POLARDB获选2019中国数据库——“年度最佳创新产品”. POLARDB是阿里云在2018年正式商业化的云原生数据 ...

  5. @hdu - 6584@ Meteor

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 询问第 k 小的分子分母 ≤ n 的既约分数. Input 第一 ...

  6. BERT大火却不懂Transformer?读这一篇就够了 原版 可视化机器学习 可视化神经网络 可视化深度学习

    https://jalammar.github.io/illustrated-transformer/ The Illustrated Transformer Discussions: Hacker ...

  7. hdu 1050 Moving Tables (Greedy)

    Problem - 1050 过两天要给12的讲贪心,于是就做一下水贪心练习练习. 代码如下: #include <cstdio> #include <iostream> #i ...

  8. jieba中文分词源码分析(四)

    一.未登录词问题在jieba中文分词的第一节曾提到未登录词问题 中文分词的难点 分词规范,词的定义还不明确 (<统计自然语言处理>宗成庆)歧义切分问题,交集型切分问题,多义组合型切分歧义等 ...

  9. 【原生JS】自动渐变轮播

    渐变主要是通过CSS3的动画实现. 只需给css中添加transtion动画时间加上JS设置指定图片透明度显示与消失即可实现渐变过程. 效果图: HTML: <!DOCTYPE html> ...

  10. 三分钟学会@Autowired@Qualifier@Primary注解

    三分钟学会@Autowired@Qualifier@Primary注解 2018.10.08 20:24 154浏览 今天主要简单的跟大家介绍一下spring自动装配相关的@Autowired,@Qu ...