go 进阶 sync相关: 七. sync.Map

一. sync.Map 基础使用示例

  1. sync.Map 的使用方法与普通的 map 类似,可以通过类似于读写锁的方式进行读写操作,提供了以下几个方法
  1. Store(key, value):向哈希表中存储一个键值对。
  2. Load(key):从哈希表中获取指定键的值。
  3. LoadOrStore(key, value):从哈希表中尝试获取指定键的值。如果该键不存在,则存储给定的键值对并返回 value。如果键已经存在,则直接返回当前值。
  4. Delete(key):从哈希表中删除指定键及其对应的值。
  5. Range():遍历哈希表中的所有键值对,并调用指定的处理函数进行处理。
  1. sync.Map通过读写分离的机制,在读取时不需要加锁,在写入时则会进行细粒度的锁定,以保证数据的一致性和并发安全性
  2. 注意: 使用 sync.Map 时不需要调用 make() 来初始化。因为 sync.Map 的 zero value 就是一个可用的空 map,可以直接对其进行 Store、Load、Delete 和 Range 等操作
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	// 存储键值对
	m.Store("foo", 1)
	m.Store("bar", "hello")

	// 根据键获取值
	if v, ok := m.Load("foo"); ok {
		fmt.Println(v)
	}

	// 删除键值对
	m.Delete("bar")

	// 遍历所有的键值对
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("key: %v, value: %v\n", key, value)
		return true
	})
}

二. 底层

  1. 不同版本的底层实现是不同的
  2. 查看sync.Map底层结构
type Map struct {
 mu Mutex
    
 // 把read看成一个安全的只读快照表,实际对应的是readOnly, 
 read atomic.Value // readOnly

 // dirty需要使用上面的mu加锁才能访问里面的元素,
 //dirty中包含所有在read字段中但未被expunged(删除)的元素,
 //重点包含最新的 KV 对,等时机成熟,dirty 会被转换为 read, 然后该字段会被置为空
 dirty map[interface{}]*entry

 // misses是一个计数器,记录在从read中读取数据的时候,没有命中的次数,
 //每次从 read 中没找到回到 dirty 中查询都会导致 misses 自增一,
 //当misses > len(dirty) 时,就会触发dirty转换
 misses int
}
  1. Map中的read字段是atomic.Value类型,它里面实际存储的结构为readOnly,readOnly以原子方式存储在read中
// readOnly是存在Map结构中read字段中的内容,它以原子方式存储
type readOnly struct {
 m map[interface{}]*entry
 // amended为true表示dirty中包含read中没有的数据,
 //为false表示dirty中的数据在read都存在
 amended bool // true if the dirty map contains some key not in m.
}

// dirty 代表状态不稳定的哈希表,即正在执行写操作的哈希表
type dirty struct {
    m       map[interface{}]*entry
    dirty   map[interface{}]*entry // 暂存 dirty 表中需要删除的元素
    misses int                    // 记录 readOnly 中查找 fail + dirty 中查找 success 的次数
}
  1. 无论是dirty还是read中的m它们都是内建的map,在存储value时会将value封装为entry类型。entry是对interface{}做了一个结构体的包装。
type entry struct {
 p unsafe.Pointer // *interface{}
}
  1. entry中的p属性实际有三种状态分别为: nil, expunged删除,正常,
  2. 在sync.Map删除一个key时,并不是立即删除,而是将key对应的value标记为nil或者expunged,在以后的处理过程中才有机会真正删除
var expunged = unsafe.Pointer(new(interface{}))

先简述一下sync.Map如何实现线程安全的

  1. 在java中ConcurrentHashMap1.7版本是通过分段锁,1.8版本是通过锁桶的首节点+synchronized实现的线程安全,通过降低锁粒度来提升性能,sync.Map 的思路是尽量通过读写分离与原子操作实现线程安全
  2. 在sync.Map中提供了:
  1. read属性是一个atomic.Value原子类型,内部存储的实际是一个map
  2. dirty属性: 需要加锁访问,包含所有在read字段,重点是存储了最新的 KV 对,一个新的键值对会被存储在这,等时机成熟,dirty 会被转换为 read, 然后该字段会被置为空,由于 dirty 中的数据总是比 read 中的更新,所以在查询修改等操作中,read 中如果找不到还需要回到 dirty 中找
  3. mu: 用于对 dirty 操作时保障并发安全的锁
  4. misses 计数器属性用来控制什么时候将dirty 转换为 read
  1. 在存储KV键值对时,数据首先保存到dirty中,dirty需要加锁访问,所以在查询修改等时,如果read 中不存在需要通过 dirty查找,通过misses 计数器记录通过read读取数据没有命中的次数,当misses与dirty长度相同时会把dirty转换为 read, 然后将dirty置为空,read 与 dirty 中存储的 Value 都是 entry 的指针,同一个key指向的是同一个value
    在这里插入图片描述
  2. 为了提高性能,针对保存数据的entry提供了expunged 删除状态:
  1. 首先删除数据时并不是直接删除,会先打expunged 标记
  2. 为了提高性能,假设添加的key在read中已经存在但是对应的entry.p不是expunged状态,此时不加锁更新即可,但是当key对应的entry.p=expunged状态,需要在read和dirty中都添加,这个过程是需要进行加锁的,expunged状态,是为了dirty为空的时候,直接对read进行操作不用加锁,提升程序性能

Store 插入

  1. Store()添加数据时,有新增或更新两种情况,同时也有加锁与不加锁两种情况
  2. 不加锁情况:
  1. key在read中,p=nil(p是entry中的),并且dirty中不存在数据,
  2. key在read中,p=&entry,表示key存在,且指向一个真实的value
  1. 加锁情况:
  1. key在read中,p=expunged表示key已被删除,dirty中存在数据且该key不在dirty中,需要加锁处理
  2. key不在read中 使用到了锁的时候,性能就会下降
  1. 所以sync.Map比较适合那些只会增长的缓存系统,可以进行更新操作,但最好不要删除,并且不要频繁地增加新元素,因为新增元素,会加入到dirty中,对dirty操作需要加锁。在dirty为nil,新加元素的时候,会创建一个新dirty,将read中的有效的key-value键值对复制到新dirty中,read中已删除的key(value为nil或者expunged)不会复制到dirty中
  2. sync.Map基于类似读写分离的机制,在读取时不需要加锁,在写入时则会进行细粒度的锁定,以保证数据的一致性和并发安全性,查看Store()源码:
  1. 首先,在readOnly只读快照中查找键是否存在,如果存在调用entry上的tryStore()方法修改值
  2. 如果readOnly只读快照中不存在,则通过Map中提供的mu写入锁属性调用Lock()函数加锁
  3. 加锁后首先执行"m.read.Load().(readOnly)", 通过readOnly再次判断是否存在,并且检查是否有其他协程在并发操作
  4. 如果readOnly中存在,调用entry上的storeLocked()方法修改该key的value,并且会判断当前key如果是expunged状态(表示该键所对应的值已经被删除,只有在 dirty 部分中才会出现该状态),从 dirty 中移除,并加入readOnly中
  5. 如果没有在readOnly中,会判断是否在dirty 中,存在则修改其 value
  6. 如果 key 既不在 readOnly 中,也不在 dirty 中,则调用newEntry()函数,将value封装为entry结构体变量插入到 dirty 中,如果dirty为空,还需要进行初始化,并将 readOnly 部分的 amended 标记设置为 true
  7. 最后释放锁,并且通过 CAS 原子操作更新 readOnly 部分,将 dirty 部分更新为最新的 readOnly
  8. 注意在存储value时,会将value封装为一个entry结构体,比如如果key不存在会调用newEntry()将value封装为entry结构体,如果key存在会调用entry上的storeLocked()修改
// Store保存或更新一个键值对
func (m *Map) Store(key, value interface{}) {
 // 检查key是否在read中存在
 read, _ := m.read.Load().(readOnly)
 // 如果key在read中,有3种情况:
 // 1.p=nil,表示key已删除,并且dirty中不存在数据
 // 2.p=expunged,表示key已删除,dirty中存在数据且该key不在dirty中
 // 3.p=&entry,表示key存在,指向一个真实的value
 // 对情况1和情况3,直接将value的值存在p中,对情况2不存value,继续走后面的逻辑
 if e, ok := read.m[key]; ok && e.tryStore(&value) {
  return
 }

 m.mu.Lock()
 // 加锁后,继续检查read中是否有key存在
 read, _ = m.read.Load().(readOnly)
 // key在read中,继续检查key是否已经被删除
 if e, ok := read.m[key]; ok {
 //2. key对应的值被标记为expunged,read中的entry拷贝到dirty时,
 //会将key标记为expunged,需要手动解锁unexpungeLocked()
  if e.unexpungeLocked() {
   // 如果key已被删除,并且处于expunged状态,说明此key存在read但不在dirty中
   // 并且此时dirty非空,需要将此key加入到dirty中,并且更新e.p的值指向value
   m.dirty[key] = e
  }
  e.storeLocked(&value)
 } else if e, ok := m.dirty[key]; ok {
  // key不在read中但在dirty中,直接更新dirty中e.p的值,指向value
  e.storeLocked(&value)
 } else {
  //走到这里说明key既不在read中也不在dirty中,肯定是一个新的key.
  //并且dirty中所有的key都在read中
  if !read.amended {
   // 如果dirty为nil,需要创建dirty对象,并且标记read的amended为true,
   // 说明有元素存在于dirty中但不在read中
   m.dirtyLocked()
   m.read.Store(readOnly{m: read.m, amended: true})
  }
  // new一个新entry,将新值加入到dirty对象中
  m.dirty[key] = newEntry(value)
 }
 m.mu.Unlock()
}

// tryStore尝试将value的值存在e.p中
func (e *entry) tryStore(i *interface{}) bool {
 for {
  p := atomic.LoadPointer(&e.p)
  // 如果p为expunged,不能直接存储,因为此时的read中所有处于非expunged状态的key都
  // 在dirty中,将key加回到read的时候,也需要将其加入到dirty中,此处不处理这种情况
  // 直接返回
  if p == expunged {
   return false
  }
  // p为nil或指向&entry对象,设置e.p为i的值,即将e.p指向存入的value
  if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
   return true
  }
 }
}

// unexpungeLocked将e.p从expunged修改为nil
func (e *entry) unexpungeLocked() (wasExpunged bool) {
 return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

// storeLocked原子操作,将i存储到e.p中,此处的i不能是expunged值
func (e *entry) storeLocked(i *interface{}) {
 atomic.StorePointer(&e.p, unsafe.Pointer(i))
}

tryStore()key存在时的更新

  1. 在通过Store()插入数据时,如果key已经存在会调tryStore(),该函数中首先会判断key对应的值的状态,如果是expunged需要原子更新
func (e *entry) tryStore(i *interface{}) bool {
    p := atomic.LoadPointer(&e.p)
  // 这个entry是key对应的entry,p是key对应的值,如果p被设置为expunged,不能直接更新存储
    if p == expunged {
        return false
    }
    for {
    // 原子更新
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
        if p == expunged {
            return false
        }
    }
}

dirtyLocked()将read中的数据同步到dirty

  1. 当添加的数据不在read并且也不在dirty时,如果dirty为nil需要新建,还有一种情况数据在dirty不在read时需要同步数据,将read中的数据同步到dirty,该函数内部会遍历获取read中的所有数据,执行tryExpungeLocked()通过原子操作给read中的数据设置expunged标记,如果不是expunged状态则添加到dirty
func (m *Map) dirtyLocked() {
  // dirty != nil 说明dirty在上次read同步dirty数据后,已经有了修改了,这时候read的数据不一定准确,不能同步
    if m.dirty != nil {
        return
    }

    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    //遍历获取read中的所有数据
    for k, e := range read.m {
    	//这里调用tryExpungeLocked 来给entry,即key对应的值 设置标志位
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

//通过原子操作,给entry,key对应的值设置 expunged 标志
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

添加时的多种情况总结

  1. 在执行Store()添加数据时有多种情况:
  1. key原先就存在于read中,获取key所对应内存地址,原子性修改
  2. key存在,但是key所对应的值被标记为 expunged,解除标记,并更新dirty中的key,与read中进行同步,然后修改key对应的值
  3. read中没有key,但是dirty中存在这个key,直接修改dirty中key的值
  4. read和dirty中都没有值,先判断自从read上次同步dirty的内容后有没有再修改过dirty的内容,没有的话,先同步read和dirty的值,然后添加新的key value到dirty上面
  1. 针对第四种情况,既然read.amended == false表示数据没有修改,为什么还要将read的数据同步到dirty里面呢

答案在Load 函数里面,因为read同步dirty的数据的时候,是直接把dirty指向map的指针交给了read.m,然后将dirty的指针设置为nil,所以同步之后dirty就为nil

Load 查询

  1. 查看Load()获取指定键值的源码,实现思路和 Store() 方法类似,都是先从只读部分查找,如果没找到则再从 dirty 部分查找。不同之处在于,Load() 方法只涉及到读操作,并且不需要进行插入或者删除操作
  1. 执行"m.read.Load().(readOnly)"在只读快照中查找对应的键是否存在。如果存在,则直接返回其所对应的值
  2. 如果只读部分不存在通过mu调用Lock()加锁
  3. 加锁后会重新从只读部分中加载数据。如果找到了对应的键,则直接返回其所对应的值;
  4. 否则,从 dirty 部分中查找是否存在对应的键值对,如果存在,则返回其所对应的值,并解锁
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 先从只读部分中查找 key。
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        // 如果 key 存在于只读部分,则直接返回对应的 value。
        return e.load()
    }
    // 加锁
    m.mu.Lock()
    //重新从只读部分中加载数据
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        m.mu.Unlock()
        return e.load()
    }
    //如果readOnly中不存在,会通过dirty部分查找
    if e, ok := m.dirty[key]; ok {
    	//如果dirty中存在,解锁
        m.mu.Unlock()
        //并返回,返回时需要采用乐观锁策略来确保并发安全性
        return e.load()
    }
    // 如果都没有找到,则返回 nil。
    m.mu.Unlock()
    return nil, false
}
  1. 当通过read未查询到数据时,会执行missLocked()增加misses计数器,当misses与dirty长度相同时会把dirty转换为read,并将dirty置为空
  2. 问题: 为什么找到了entry.p,但是p对应的值为nil呢?答案在Delete函数中

missLocked() 累加计数与同步dirty数据到read

  1. 在调用Load()读取数据时,如果read未命中,并且dirty中存储数据时(有修改)时会调用missLocked()累加misses计数器,当m.misses 等于dirty长度时,直接把dirty的指针给read.m,并且设置dirty为nil,misses计数重置为0
func (m *Map) missLocked() {
 // misses计数+1
 m.misses++
 // 如果没有达到临界值(dirty的长度),直接返回
 if m.misses < len(m.dirty) {
  return
 }
 // 将dirty字段的内容提升为read
 //直接把dirty的指针给read.m,并且设置dirty为nil,
 //这里也就是 Store 函数的最后会调用 m.dirtyLocked的原因
 m.read.Store(readOnly{m: m.dirty})
 // 清空dirty,dirty为map类型,清空方法是直接赋值nil,让GC清理掉里面的内容
 m.dirty = nil
 // misses计数重置为0
 m.misses = 0
}

Delete 删除

  1. 在删除时同样还是优先检查key是否在read中,在read有两种情况
  1. 一种是此key只在read中,不在dirty中,将entry.p标记为expunged删除状态,方便后操作相同key直接修改read中e.p的值
  2. 一种是此key也存在dirty中,此时dirty中key对应的e和read中该key对应的e是同一个,这种情况将e.p设置为nil
  1. 如果删除的key不在read中,当前dirty又不为空,此时需要进一步确认key是否在dirty中,加锁处理,如果key在dirty中,直接调用delete将dirty中的key删除
  2. 可以重复调用Delete操作删除同一个key,只有第一次会标记删除,后面调用不做处理
  3. 查看Delete()源码,内部会调用Map上的LoadAndDelete(),在该方法中:
  1. 首先会通过readOnly只读快照判断key是否存在
  2. 如果不存在,并且 amended 标志位为 true说明存在更新,则加锁处理
  3. 加锁后会通过readOnly在判断一次,如果还不存在,会查询 dirty中是否包含该键值对
  4. 如果dirty中包含,会调用一个delete()函数,从 dirty map 中查找该键值对,并调用missLocked()等待dirty map被提升至readOnly 中
  5. 如果在 readOnly 或 dirty 中找到了该键值对,则调用entry上的delete()方法进行删除操作
func (m *Map) Delete(key interface{}) {
	m.LoadAndDelete(key)
}

func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
	//通过readOnly只读快照查找键值对
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	//如果在 readOnly 段中未找到该键值对,并且 amended 标志位为 true说明存在更新,
	//则加锁,查询 dirty中是否包含该键值对。
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		//在锁定状态下再次检查readOnly只读快照中是否存在
		e, ok = read.m[key]
		if !ok && read.amended {
			// 如果仍未找到,并且 amended 标志位为 true,从 dirty map 中查找该键值对
			e, ok = m.dirty[key]
			if ok {
				// 如果在 dirty中找到了该键值对,将该键值对从 dirty map 中删除
				delete(m.dirty, key)
				//无论该键值对是否存在,都要调用 missLocked 方法
				//该方法中会对dirty 中的misses进行累加
				//如果达到阈值会将dirty提升为read
				m.missLocked()
			}
		}
		// 操作结束后解锁
		m.mu.Unlock()
	}

	if ok {
		 // 如果在 readOnly 或 dirty 中找到了该键值对,则调用entry上的delete()方法进行删除操作。
		 // 满足ok为true,read中肯定是有该key的, dirty有两种情况:
 		 // 情况1,dirty中没有该key,因为dirty中不存,直接将read中e.p设置为nil,标记为删除状态
 		 // 情况2,dirty中也有该key,dirty中key对应的e和read中该key对应的e是同一个,所以直接将
 		 // read中的e.p设置为nil,其实也是将dirty中e.p也设置为nil了
		return e.delete()
	}
	// 如果在readOnly 和 dirty map 中都未找到该键值对,则返回失败。
	return nil, false
}

删除时的多种情况总结

  1. read中没有,且Map存在修改,则尝试删除dirty中的map中的key
  2. read中没有,且Map不存在修改,那就是没有这个key,无需操作
  3. read中有,尝试将key对应的值设置为nil,后面读取的时候就知道被删了,因为dirty中map的值跟read的map中的值指向的都是同一个地址空间,所以,修改了read也就是修改了dirty

Range 遍历

  1. 调用遍历时需要传入一个func(key, value interface{}) bool类型的函数, 对遍历到的每个键值对调用f进行处理,当传入的函数返回false则停止
  2. Map只有两种状态.被修改过和没有修改过
  1. 修改过:将dirty的指针交给read,read就是最新的数据了,然后遍历read的map
  2. 没有修改过:遍历read的map就好了
// Range 方法对sync.Map进行遍历操作,需要传入一个func(key, value interface{}) bool类型的
// 函数f,会对遍历到的键值对调用f进行处理,如果函数f返回false,对sync.Map的迭代将停止。
// Range 方法在遍历的时候会对sync.Map的元素至多访问一次,如果在执行Range操作的时候,有其他协程并发
// 的添加或删除元素,可能会导致有些元素未被遍历到。
// Range 方法是一个O(N)时间复杂度的操作,对于存在元素在dirty不在read的情况,进行了一个优化,将dirty
// 提升为read了,所以下次在进行Range的时候,直接对read进行遍历,不用加锁。
func (m *Map) Range(f func(key, value interface{}) bool) {
 
 // 如果所有的元素都在read中,直接对read进行遍历
 read, _ := m.read.Load().(readOnly)
 // 确认是否有元素存在dirty中而不在read中
 if read.amended {
  m.mu.Lock()
  read, _ = m.read.Load().(readOnly)
  if read.amended {
   // 有元素在dirty中,对dirty进行遍历
   read = readOnly{m: m.dirty}
   // 进行一个优化,将dirty提升为read
   m.read.Store(read)
   // 将dirty提升为read之后,dirty置为nil
   m.dirty = nil
   // 计数器清理0
   m.misses = 0
  }
  m.mu.Unlock()
 }
    // 对遍历到的每个元素,调用传入的函数f进行处理
 for k, e := range read.m {
  v, ok := e.load()
  if !ok {
   continue
  }
  if !f(k, v) {
   break
  }
 }
}

LoadOrStore()存在则获取,不存在则添加

  1. LoadOrStore方法可以看做Load操作和Store操作的组合,如果key已经在sync.Map中,返回当前key对应的value,否则将存储传入的value值。
// LoadOrStore 可以看做Load操作和Store操作的组合,如果key已存在m中(无论是在read中还是dirty中),
// 就是只要key没有被删除,就返回当前的key对应的value值,否则将存储传入的value值,
// 第二返回参数是一个bool值,表示最后执行的是load操作还是store操作
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
 // 还是优先检查read,避免加锁
 read, _ := m.read.Load().(readOnly)
 if e, ok := read.m[key]; ok {
  // 尝试load和store操作,如果ok为true,表示load成功
  actual, loaded, ok := e.tryLoadOrStore(value)
  if ok {
   return actual, loaded
  }
 }

 m.mu.Lock()
 // 双重检查
 read, _ = m.read.Load().(readOnly)
 if e, ok := read.m[key]; ok {
  // e.p为expunged状态,表示m.dirty非空且dirty不存在该key
  // 需要将key-value加到dirty中,这里dirty和read实际上key
  // 指向的是同一个e,更新e值,dirty和read中都存有该key-value了
  if e.unexpungeLocked() {
   m.dirty[key] = e
  }
  actual, loaded, _ = e.tryLoadOrStore(value)
 } else if e, ok := m.dirty[key]; ok {
  // key在dirty中不在read中,
  actual, loaded, _ = e.tryLoadOrStore(value)
  m.missLocked()
 } else {
  if !read.amended {
   m.dirtyLocked()
   m.read.Store(readOnly{m: read.m, amended: true})
  }
  m.dirty[key] = newEntry(value)
  actual, loaded = value, false
 }
 m.mu.Unlock()

 return actual, loaded
}

func (e *entry) tryLoadOrStore(i interface{}) (actual interface{}, loaded, ok bool) {
 p := atomic.LoadPointer(&e.p)
 // key已被删除,需要进一步判断处理,这里先终止处理
 if p == expunged {
  return nil, false, false
 }
 // key存在,value值有效,直接返回之前的value值,即执行Load操作
 if p != nil {
  return *(*interface{})(p), true, true
 }

 // 走到这里说明key也是已经被删除,e.p为nil,并且dirty是空的,所以直接将i存储在e.p中即可,不用关心dirty
 ic := i
 for {
  // 原子更新e.p的值,更新前为nil
  if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) {
   return i, false, true
  }
  // 进一步判断e.p是不是被别的地方已经修改为非nil了
  p = atomic.LoadPointer(&e.p)
  // 如果p为expunged,说明key在其他地方已经被删除了,需要进一步判断处理,这里先终止处理
  if p == expunged {
   return nil, false, false
  }
  // e.p已经被其他地方设置值了,这里直接返回已设置的值
  if p != nil {
   return *(*interface{})(p), true, true
  }
 }
}

sync.Map底层的entry

  1. 查看sync.Map的插入Store, 查询Load, 删除Delete底层源码,内部都会执行entry上的方法比如
  1. 在Store()插入时,首先会判断当前插入的key是否存在,如果不存在会调用一个newEntry()函数,将value封装为entry结构体变量,如果存在会调用entry上的tryStore()尝试更新,会调用storeLocked()方法更新value,将value保存到Map底层的dirty 或 readOnly,最后通过 CAS 原子操作更新 readOnly 部分,将 dirty 部分更新为最新的 readOnly
  2. 在Load()查询时,如果当前key存在readOnly或dirty中,会拿到一个entry结构体变量,调用load()采用乐观锁策略来确保并发安全的返回数据
  3. 在Delete()删除时,如果key存在也会先拿到对应的entry变量,通过entry上的delete()方法进行删除
  1. entry.tryStore()尝试更新源码: 通过循环CAS操作不断尝试将 entry 的指针指向一个新的值,实现更新
// tryStore 方法用于更新节点,返回更新是否成功的标记。
func (e *entry) tryStore(i *interface{}) bool {
	// 通过循环 CAS 操作不断尝试更新 entry 的指针 p,直到成功或者发现已经被删除。
	for {
		p := atomic.LoadPointer(&e.p)  // 获取 entry 的指针 p
		if p == expunged {  // 如果 p 被标记为删除,则直接返回 false 表示更新失败。
			return false
		}
		// 使用 CAS 原子操作更新 entry 的指针 p
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {  
			return true   // 返回 true 表示更新成功
		}
	}
}
  1. entry.storeLocked():使用原子类修改value
func (e *entry) storeLocked(i *interface{}) {
	atomic.StorePointer(&e.p, unsafe.Pointer(i))
}
  1. entry.load():获取value,乐观锁策略来确保并发安全的返回数据
// load 方法用于获取 entry 的指针,并将其转换为 interface{} 类型的值和一个布尔值返回。
func (e *entry) load() (value interface{}, ok bool) {
	p := atomic.LoadPointer(&e.p)  // 获取 entry 的指针 p
	// 如果 p 指向的值为 nil 或者 expunged,则说明该 entry 已经被删除或尚未创建。
	if p == nil || p == expunged {  
		return nil, false   // 直接返回 false 表示加载失败
	}
	// 将 entry 的指针转换为 interface{} 类型的值并返回,同时返回一个 true 标识加载成功
	return *(*interface{})(p), true   
}
  1. entry.delete()删除源码: 通过循环 CAS 操作不断尝试更新 entry 的指针 p,直到成功或者发现已经被删除
// delete 方法用于删除节点,返回被删除节点的值及删除成功标记。
func (e *entry) delete() (value interface{}, ok bool) {
	// 通过循环 CAS 操作不断尝试更新 entry 的指针 p,直到成功或者发现已经被删除。
	for {
		p := atomic.LoadPointer(&e.p)  // 获取 entry 的指针 p
		// 如果 p 为 nil 或者已经被删除,则说明该 entry 已经不存在,直接返回 false
		if p == nil || p == expunged {  
			return nil, false
		}
		// 如果成功将 p 设置为 nil,说明删除成功
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {  
			return *(*interface{})(p), true   // 返回被删除节点的值及删除成功标记
		}
	}
}

三. 总结

  1. sync.Map是一个支持并发安全的Map键值对映射表,提供了Store(key, value)插入,Load(key)获取,Delete(key)删除,Range()遍历等方法,了解底层首先要了解内部结构,内部包含了
  1. read属性,实际在操作时对应的是一个readOnly可以看为一个只读快照表,每个正在读取的 Goroutine 都会持有一个 readOnly 结构体
  2. dirty属性,修改数据时的一个可写表,可以用来判断当前当readOnly只读副本是否失效,并且多个 Goroutine 同时操作时,每个 Goroutine 都会持有一个 dirty 表
  3. mu锁属性防止并发写入时互相干扰
  4. 实际在readOnly与dirty中保存数据时会将value封装为entry结构体变量
  5. 在readOnly中还存在一个amended标识:为true时,表示dirty中存在read中没有的键值对
  6. 怎么保证并发安全的,可以看一下Store(key, value)插入和Load(key)获取源码
  1. 插入时在Store(key, value)源码中
  1. 首先,在readOnly只读快照中查找键是否存在,如果存在调用entry上的tryStore()方法通过cas修改值
  2. 如果readOnly只读快照中不存在,则通过Map中提供的mu写入锁属性调用Lock()函数加锁
  3. 加锁后双重检测再次通过readOnly判断是否存在,并且检查是否有其他协程在并发操作
  4. 如果readOnly中存在,调用entry上的storeLocked()方法修改该key的value,并且会判断当前key如果是expunged状态(表示该键所对应的值已经被删除,只有在 dirty 部分中才会出现该状态),从 dirty 中移除,并加入readOnly中
  5. 如果没有在readOnly中,会判断是否在dirty 中,存在则修改其 value
  6. 如果 key 既不在 readOnly 中,也不在 dirty 中,则调用newEntry()函数,将value封装为entry结构体变量插入到 dirty 中,如果dirty为空,还需要进行初始化,并将 readOnly 部分的 amended 标记设置为 true
  7. 最后释放锁,并且通过 CAS 原子操作更新 readOnly 部分,将 dirty 部分更新为最新的 readOnly
  8. 注意在存储value时,会将value封装为一个entry结构体,比如如果key不存在会调用newEntry()将value封装为entry结构体,如果key存在会调用entry上的storeLocked()修改
  1. 获取时在Load(key)源码中
  1. 执行"m.read.Load().(readOnly)"在只读快照中查找对应的键是否存在。如果存在,则直接返回其所对应的值
  2. 如果只读部分不存在通过mu调用Lock()加锁
  3. 加锁后会重新从只读部分中加载数据。如果找到了对应的键,则直接返回其所对应的值;
  4. 否则,从 dirty 部分中查找是否存在对应的键值对,如果存在,则返回其所对应的值,并解锁
  1. 通过Store(key, value)与Load(key)总结
  1. 在map内部提供了readOnly只读列表与dirty可写列表另外还有保护dirty可写列表的mu锁
  2. 基于读写分离的思想,在插入与读取时首先会基于readOnly读取或修改,如果readOnly中不存在,再加锁通过dirty处理
  3. 如果dirty处理完毕后,会将dirty提升到readOnly中
  4. 并且readOnly与dirty底层存储value时会将value封装成一个entry结构体变量,在增删改查时会调用entry下的大量方法,这些方法内部会通过原子类,cas等在保证性能的前提下以无锁的形式保证并发安全
  1. entry下的几个方法总结:
  1. load() 方法:用于获取 entry 的值。通过原子操作保证了其并发安全性。
  2. store() 方法:用于更新 entry 的指针。通过循环 CAS 操作来尝试更新 entry 的指针,通过原子操作保证了并发安全
  3. tryStore() 方法:与 store() 方法类似,用于更新 entry 的指针。不同之处在于,tryStore() 方法会尝试复用旧的 entry 对象,从而节省内存开销。通过循环 CAS 操作来尝试更新 entry 的指针,并通过原子操作保证了其并发安全性。
  4. delete() 方法:用于删除 entry,并将其指针标记为 nil。将 entry 标记为删除的目的是为了避免在删除过程中出现 race condition。通过循环 CAS 操作来尝试将 entry 的指针标记为 nil,并通过原子操作保证了其并发安全性。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值