golang 解读(2) sync.Map

本文详细介绍了Go 1.9版本中引入的sync.Map特性,包括其内部实现原理、常见用法及优化点。sync.Map是一种线程安全的映射类型,特别适合读多写少的并发场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

sync.map就是1.9版本带的线程安全map.

[[在Go 1.6之前, 内置的map类型是部分goroutine安全的,并发的读没有问题,并发的写可能有问题。自go 1.6之后, 并发地读写map会报错,

所以go 1.9之前的解决方案是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

在Go官方blog的Go maps in action一文中,提供了一种简便的解决方案。

var counter = struct{
    sync.RWMutex  //读写锁
    m map[string]int
}{m: make(map[string]int)}

它使用嵌入struct为map增加一个读写锁。

读数据的时候很方便的加锁:

counter.RLock() // 读锁定
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

写数据的时候:

unter.Lock()// 写锁定
counter.m["some_key"]++
counter.Unlock()

golang中sync包实现了两种锁

Mutex (互斥锁:适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁叶叫做全局锁)

和RWMutex(读写锁:该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景;如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定

区别参见:http://blog.youkuaiyun.com/chenbaoke/article/details/41957725

]]

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

用法:

Store(key,value interface{})

说明: 存储一个设置的键值

LoadOrStore(key,value interface{}) (actual interface{}, loaded bool)

说明:返回键的现有值(如果存在),否则存储并返回给定的值,如果是读取则返回true,如果是存储返回false

Load(keyinterface{}) (value interface{}, ok bool)

说明:读取存储在map中的值,如果没有值,则返回nilOK的结果表示是否在map中找到值

Delete(keyinterface{})

说明: 删除键对应的值

Range(ffunc(key, value interface{}) bool)

说明: 循环读取map中的值

sync.Map的实现有几个优化点,

空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。 使用只读数据(read),避免读写冲突。 动态调整,miss次数多了之后,将dirty数据提升为read。 double-checking。 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。 优先从read读取、更新、删除,因为对read的读取不需要锁。
参考:https://studygolang.com/articles/10511

sync.Map的数据结构,它使用了冗余的数据结构read、dirty。dirty中会包含read中为删除的entries,新增加的entries会加入到dirty中。

type Map struct {
    // 当涉及到dirty数据的操作的时候,需要使用这个锁
    mu Mutex
    // 一个只读的数据结构,因为只读,所以不会有读写冲突。从这个数据中读取总是安全的。
    // 实际上,也会更新这个数据的entries,如果entry是未删除的(unexpunged), 并不需要加锁。如果entry已经被删除了,需要加锁,以便更新dirty数据。
    read atomic.Value // readOnly
    // dirty数据包含当前的map包含的entries,它包含最新的entries(包括read中未删除的数据,虽有冗余,但是提升dirty字段为read的时候非常快,不用一个一个的复制,而是直接将这个数据结构作为read字段的一部分),
    // 有些数据还可能没有移动到read字段中。对于dirty的操作需要加锁,因为对它的操作可能会有读写竞争。
    // 当dirty为空的时候, 比如初始化或者刚提升完,下一次的写操作会复制read字段中未删除的数据到这个数据中。
    dirty map[interface{}]*entry
    // 当从Map中读取entry的时候,如果read中不包含这个entry,会尝试从dirty中读取,这个时候会将misses加一,
    // 当misses累积到 dirty的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。
    misses int
}

read的数据结构是:

type readOnly struct {
    m       map[interface{}]*entry
    amended bool // 如果Map.dirty有些数据不在中的时候,这个值为true
}

amended指明Map.dirty中有readOnly.m未包含的数据,所以如果从Map.read找不到数据的话,还要进一步到Map.dirty中查找。

使用列子:

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    // 1.首先从m.read中得到只读readOnly,从它的map中查找,不需要加锁
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    // 2. 如果没找到,并且m.dirty中有新数据,需要从m.dirty查找,这个时候需要加锁
    if !ok && read.amended {
        m.mu.Lock()
        // 双检查,避免加锁的时候m.dirty提升为m.read,这个时候m.read可能被替换了。
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        // 如果m.read中还是不存在,并且m.dirty中有新数据
        if !ok && read.amended {
            // 从m.dirty查找
            e, ok = m.dirty[key]
            // 不管m.dirty中存不存在,都将misses计数加一
            // missLocked()中满足条件后就会提升m.dirty
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}
func (m *Map) Store(key, value interface{}) {
    // 如果m.read存在这个键,并且这个entry没有被标记删除,尝试直接存储。
    // 因为m.dirty也指向这个entry,所以m.dirty也保持最新的entry。
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }
    // 如果`m.read`不存在或者已经被标记删除
    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() { //标记成未被删除
            m.dirty[key] = e //m.dirty中不存在这个键,所以加入m.dirty
        }
        e.storeLocked(&value) //更新
    } else if e, ok := m.dirty[key]; ok { // m.dirty存在这个键,更新
        e.storeLocked(&value)
    } else { //新键值
        if !read.amended { //m.dirty中没有新的数据,往m.dirty中增加第一个新键
            m.dirtyLocked() //从m.read中复制未删除的数据
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value) //将这个entry加入到m.dirty中
    }
    m.mu.Unlock()
}
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
        }
    }
}
func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        // 将已经删除标记为nil的数据标记为expunged
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}
以上操作都是先从操作m.read开始的,不满足条件再加锁,然后操作m.dirty。

Store可能会在某种情况下(初始化或者m.dirty刚被提升后)从m.read中复制数据,如果这个时候m.read中数据量非常大,可能会影响性能。



### Golang `sync.Map` 与 Java `ConcurrentHashMap` 性能对比分析 #### 1. 数据结构设计 Golang 的 `sync.Map` 是一种专门为并发场景设计的非泛型映射类型,其内部实现基于分段锁(sharded locking)和无锁读取优化[^2]。相比之下,Java 的 `ConcurrentHashMap` 使用了更复杂的分段锁机制(Segmented Locking),并且在 JDK 8 中引入了 CAS 操作和红黑树来进一步提升性能[^1]。 #### 2. 并发控制策略 `sync.Map` 在写操作时使用互斥锁(Mutex)进行同步,而在读操作时尽量避免加锁,从而减少竞争开销。这种设计使得 `sync.Map` 在高并发读多写少的场景下表现出色。 `ConcurrentHashMap` 则通过将哈希表分为多个段(Segment),每个段独立加锁,从而允许多个线程同时访问不同的段。此外,在 JDK 8 中,`ConcurrentHashMap` 进一步优化了锁粒度,改为基于节点的锁,并结合 CAS 操作提高吞吐量[^1]。 #### 3. 内存占用 由于 Golang 的 `map` 实现更加紧凑,`sync.Map` 在内存管理上也更为高效。它动态调整桶的大小以减少空间浪费,同时避免了 Java 中可能因反射和缓存导致的额外内存开销。 `ConcurrentHashMap` 虽然在性能上有所优化,但其复杂的分段锁机制和红黑树转换逻辑可能导致更高的内存消耗,尤其是在键值对数量较少的情况下[^1]。 #### 4. 性能测试结果 在低并发场景下,`ConcurrentHashMap` 和 `sync.Map` 的性能差异不大,因为此时锁的竞争较少。然而,在高并发场景下,`sync.Map` 的无锁读取优化使其表现优于 `ConcurrentHashMap`,尤其是在读多写少的情况下[^2]。 以下是简单的基准测试代码示例: ```go // Golang sync.Map 基准测试 package main import ( "fmt" "sync" "sync/atomic" "testing" ) func BenchmarkSyncMap(b *testing.B) { var sm sync.Map var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { defer wg.Done() for j := 0; j < b.N; j++ { sm.Store(fmt.Sprintf("key%d", j), j) _, _ = sm.Load(fmt.Sprintf("key%d", j)) } }() } wg.Wait() } ``` ```java // Java ConcurrentHashMap 基准测试 import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ConcurrentHashMapBenchmark { public static void main(String[] args) throws InterruptedException { ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 10; i++) { executor.submit(() -> { for (int j = 0; j < 1_000_000; j++) { map.put("key" + j, j); map.get("key" + j); } }); } executor.shutdown(); } } ``` #### 5. 使用场景选择 - 如果需要频繁的读操作且写操作较少,`sync.Map` 是更好的选择,因为它能够有效减少锁的竞争[^2]。 - 对于复杂的键值对存储需求,尤其是需要支持泛型和灵活数据结构的场景,`ConcurrentHashMap` 更具优势。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值