Skip to content

Commit ad02b49

Browse files
committed
add map.md
1 parent 3901577 commit ad02b49

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

sync/map.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
Golang sync map
2+
在golang中,线程安全的map实现为sync.Map,相较于java中线程安全的map ConcurrentHashMap,在设计与实现上都有巨大的差别。
3+
## java中的ConcurrentHashMap
4+
java中的ConcurrentHashMap为了实现线程安全,在1.7当中,通过分段锁的实现达到了这一目的,区别于HashTable的全部阻塞操作,分段锁的设计在一定程度上提升了在并发场景下的访问性能。在1.8的过程中,锁的粒度被进一步降低,被缩小到了一个HashEntry首节点的地步,并通过在一定长度的链表后引入红黑树来降低遍历寻找数据的开销。
5+
但是在golang中的线程安全的sync.Map并没有参考java中的实现,其设计反倒更加类似java中的CopyOnWriteArrayList,但是也不尽然,虽然sync.Map也冗分离了一部分的存储,但是包含更复杂的设计。
6+
## sync.Map的总体结构
7+
````
8+
type Map struct {
9+
// 对于readOnly的操作都不需要加锁,但是后面所有对于dirty的操作都需要这里的mu加锁。
10+
mu Mutex
11+
12+
// 这里的read实际存放的是下面的readOnly结构体,当从map中寻找数据的时候,将会先从
13+
// readOnly中寻找,在readOnly中的查找都不需要加锁。因为插入数据的时候会先插入到dirty中。
14+
// 而在readOnly中的操作都是原子操作。
15+
read atomic.Value // readOnly
16+
17+
// dirty与readOnly两个map都会在sync.Map存放具体的数据,当map数据插入的时候,将会先
18+
// 插入到dirty中,直到一定条件后才会晋升到readOnly中。
19+
dirty map[interface{}]*entry
20+
21+
// 在从map中根据key寻找数据的时候,先会从readOnly寻找,如果找不到才会从dirty当中寻找,
22+
// 当在dirty中找到的时候将会misses加一。当misses与dirty的长度相等的时候,将会开始把dirty
23+
// 中的内容晋升到readOnly中
24+
misses int
25+
}
26+
27+
type readOnly struct {
28+
m map[interface{}]*entry
29+
30+
// 这个值为true的时候说明有存在于dirty,但没有存在在readOnly中的元素。
31+
amended bool
32+
}
33+
34+
var expunged = unsafe.Pointer(new(interface{}))
35+
36+
type entry struct {
37+
// 在map中,元素将以entry的形式存放在map中。当p == nil的时候,该元素已经被删除,同但是
38+
// dirty中可能还指向该元素。当p == expunged的时候,readOnly中该元素已经被删除,同时dirty中
39+
// 也没有指向该值。在正常情况下,p为具体存储的值同时dirty中也包含该值。
40+
// 当一个元素被删除的时候,将会直接将readOnly中p置nil。接下来该值被放入dirty中的时候,将
41+
// 会把该值置为expunged,并先在dirty中存放具体的值。
42+
p unsafe.Pointer // *interface{}
43+
}
44+
````
45+
## sync.Map的Store()
46+
sync.Map通过Store()来存储一个键值对。
47+
````
48+
func (m *Map) Store(key, value interface{}) {
49+
// 在Store()的一开始,先会在readOnly中寻在是否已经存在该key,如果已经存在,尝试通过
50+
// tryStore()方法对键值对进行更新。
51+
read, _ := m.read.Load().(readOnly)
52+
if e, ok := read.m[key]; ok && e.tryStore(&value) {
53+
return
54+
}
55+
56+
// 如果上面直接在readOnly中更新的操作失败,那么需要加锁,将变更操作在dirty中完成。
57+
m.mu.Lock()
58+
read, _ = m.read.Load().(readOnly)
59+
if e, ok := read.m[key]; ok {
60+
// 如果在readOnly中,该值已经是expunged,那么先通过cas将其置为nil,代表dirty已经可能存在该值的映射。
61+
if e.unexpungeLocked() {
62+
// 该值先前为expunged,说明在ditry中不存在一个非nil的映射,直接更新dirty的值与read相同。
63+
m.dirty[key] = e
64+
}
65+
// 更新key的映射到新的value,因为dirty和readOnly都持有一个e,此时dirty和readOnly都已经更新。
66+
e.storeLocked(&value)
67+
// 如果此时dirty已经持有该映射,那么直接修改该key对应的value的地址即可。
68+
} else if e, ok := m.dirty[key]; ok {
69+
// 如果该值不存在readOnly,但是在dirty中,那么直接修改dirty的值。
70+
e.storeLocked(&value)
71+
} else {
72+
// 那么在之前的readOnly和dirty中都没有找到,那么就要往dirty中插入。
73+
if !read.amended {
74+
// 如果read的amended为false,那么说明此次是第一次往dirty中插入数据,需要
75+
// 第一次进行dirty的map空间申请。
76+
m.dirtyLocked()
77+
m.read.Store(readOnly{m: read.m, amended: true})
78+
}
79+
// 往dirty中更新数据。
80+
m.dirty[key] = newEntry(value)
81+
}
82+
m.mu.Unlock()
83+
}
84+
85+
func (e *entry) tryStore(i *interface{}) bool {
86+
for {
87+
p := atomic.LoadPointer(&e.p)
88+
// 如果为expunged,说明此时dirty中也没有该值的映射,需要先更新dirty。
89+
if p == expunged {
90+
return false
91+
}
92+
// 如果readOnly中存在没有被删除的值,那么通过cas直接更新具体的值。
93+
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
94+
return true
95+
}
96+
}
97+
}
98+
````
99+
## sync.map的Load()
100+
````
101+
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
102+
read, _ := m.read.Load().(readOnly)
103+
e, ok := read.m[key]
104+
// 如果能够在readOnly中找到,那么可以直接返回
105+
if !ok && read.amended {
106+
m.mu.Lock()
107+
// 如果在readOnly中没找到,同时dirty中包含readOnly中没有的数据,那么需要加锁从dirty
108+
// 中找。由于加锁的时候可能已经有元素从dirty中晋升到了read,那么需要双重检测。
109+
read, _ = m.read.Load().(readOnly)
110+
e, ok = read.m[key]
111+
if !ok && read.amended {
112+
e, ok = m.dirty[key]
113+
// 如果在dirty中寻找到,那么需要在missLocked()中对misses加一。
114+
m.missLocked()
115+
}
116+
m.mu.Unlock()
117+
}
118+
if !ok {
119+
return nil, false
120+
}
121+
return e.load()
122+
}
123+
124+
func (m *Map) missLocked() {
125+
m.misses++
126+
if m.misses < len(m.dirty) {
127+
return
128+
}
129+
// 当misses大于dirty的长度的时候,将会把dirty的数据全部晋升到dirty中。并将dirty置为nil。
130+
// 代表当前readOnly已经包含map中的所有数据。
131+
m.read.Store(readOnly{m: m.dirty})
132+
m.dirty = nil
133+
m.misses = 0
134+
}
135+
````
136+
## 总结
137+
总体来说,sync.map通过读写分离,但是并没有像java的CopyOnWriteArrayList完全冗余一个几乎同样大小的数据来保证读写分离,而是将刚插入但是没有几次访问机会的数据隔离在dirty中,保证在执行数据访问的时候尽可能不需要阻塞。但是当需要插入更新readOnly中不存在的数据的时候,则需要对dirty来加锁保证访问,这里的加锁粒度要比java的ConcurrentHashMap要更大,但是不是每次更新都需要加锁,readOnly中的数据可以通过cas保证原子更新。还是如一开始所说,sync.map还是更适合读多写少的场景,当新key的插入十分频繁的时候,性能和普通的map加锁没有太大的区别。

0 commit comments

Comments
 (0)