golang中的map是一種數據類型,將鍵與值綁定到一塊兒,底層是用哈希表實現的,能夠快速的經過鍵找到對應的值。git
類型表示:map[keyType][valueType] key必定要是可比較的類型(能夠理解爲支持==的操做),value能夠是任意類型。
github
初始化:map只能使用make來初始化,聲明的時候默認爲一個爲nil的map,此時進行取值,返回的是對應類型的零值(不存在也是返回零值)。添加元素無任何意義,還會致使運行時錯誤。向未初始化的map賦值引發 panic: assign to entry in nil map。golang
1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 // bool 的零值是false 8 var m map[int]bool 9 a, ok := m[1] 10 fmt.Println(a, ok) // false false 11 12 // int 的零值是0 13 var m map[int]int 14 a, ok := m[1] 15 fmt.Println(a, ok) // 0 false 16 17 18 func main() { 19 var agemap[string]int 20 if age== nil { 21 fmt.Println("map is nil.") 22 age= make(map[string]int) 23 } 24 }
清空map:對於一個有必定數據的集合 exp,清空的辦法就是再次初始化: exp = make(map[string]int),若是後期再也不使用該map,則能夠直接:exp= nil 便可,可是若是還須要重複使用,則必須進行make初始化,不然沒法爲nil的map添加任何內容。算法
屬性:與切片同樣,map 是引用類型。當一個 map 賦值給一個新的變量,它們都指向同一個內部數據結構。所以改變其中一個也會反映到另外一個。做爲形參或返回參數的時候,傳遞的是地址的拷貝,擴容時也不會改變這個地址。數據庫
1 func main() { 2 exp := map[string]int{ 3 "steve": 20, 4 "jamie": 80, 5 } 6 fmt.Println("Ori exp", age) 7 newexp:= exp 8 newexp["steve"] = 18 9 fmt.Println("exp changed", exp) 10 } 11 12 //Ori age map[steve:20 jamie:80] 13 //age changed map[steve:18 jamie:80]
遍歷map:map自己是無序的,在遍歷的時候並不會按照你傳入的順序,進行傳出。數組
1 //正常遍歷: 2 for k, v := range exp { 3 fmt.Println(k, v) 4 } 5 6 //有序遍歷 7 import "sort" 8 var keys []string 9 // 把key單獨抽取出來,放在數組中 10 for k, _ := range exp { 11 keys = append(keys, k) 12 } 13 // 進行數組的排序 14 sort.Strings(keys) 15 // 遍歷數組就是有序的了 16 for _, k := range keys { 17 fmt.Println(k, m[k]) 18 }
Go中的map在能夠在 $GOROOT/src/runtime/map.go找到它的實現。哈希表的數據結構中一些關鍵的域以下所示:安全
1 type hmap struct { 2 count int //元素個數 3 flags uint8 4 B uint8 //擴容常量 5 noverflow uint16 //溢出 bucket 個數 6 hash0 uint32 //hash 種子 7 buckets unsafe.Pointer //bucket 數組指針 8 oldbuckets unsafe.Pointer //擴容時舊的buckets 數組指針 9 nevacuate uintptr //擴容搬遷進度 10 extra *mapextra //記錄溢出相關 11 } 12 13 type bmap struct { 14 tophash [bucketCnt]uint8 15 // Followed by bucketCnt keys 16 //and then bucketan Cnt values 17 // Followed by overflow pointer. 18 }
說明:每一個map的底層都是hmap結構體,它是由若干個描述hmap結構體的元素、數組指針、extra等組成,buckets數組指針指向由若干個bucket組成的數組,其每一個bucket裏存放的是key-value數據(一般是8個)和overflow字段(指向下一個bmap),每一個key插入時會根據hash算法歸到同一個bucket中,當一個bucket中的元素超過8個的時候,hmap會使用extra中的overflow來擴展存儲key。數據結構
圖中len 就是當前map的元素個數,也就是len()返回的值。也是結構體中hmap.count的值。bucket array是指數組指針,指向bucket數組。hash seed 哈希種子。overflow指向下一個bucket。併發
map的底層主要是由三個結構構成:app
mapextra的結構以下
1 // mapextra holds fields that are not present on all maps. 2 type mapextra struct { 3 // If both key and value do not contain pointers and are inline, then we mark bucket 4 // type as containing no pointers. This avoids scanning such maps. 5 // However, bmap.overflow is a pointer. In order to keep overflow buckets 6 // alive, we store pointers to all overflow buckets in hmap.extra.overflow and hmap.extra.oldoverflow. 7 // overflow and oldoverflow are only used if key and value do not contain pointers. 8 // overflow contains overflow buckets for hmap.buckets. 9 // oldoverflow contains overflow buckets for hmap.oldbuckets. 10 // The indirection allows to store a pointer to the slice in hiter. 11 overflow *[]*bmap 12 oldoverflow *[]*bmap 13 14 // nextOverflow holds a pointer to a free overflow bucket. 15 nextOverflow *bmap 16 }
其中hmap.extra.nextOverflow指向的是預分配的overflow bucket,預分配的用完了那麼值就變成nil。
bmap的詳細結構以下
bmap中全部key存在一塊,全部value存在一塊,這樣作方便內存對齊。當key大於128字節時,bucket的key字段存儲的會是指針,指向key的實際內容;value也是同樣。
hash值的高8位存儲在bucket中的tophash字段。每一個桶最多放8個kv對,因此tophash類型是數組[8]uint8。把高八位存儲起來,這樣不用完整比較key就能過濾掉不符合的key,加快查詢速度。實際上當hash值的高八位小於常量minTopHash時,會加上minTopHash,區間[0, minTophash)的值用於特殊標記。查找key時,計算hash值,用hash值的高八位在tophash中查找,有tophash相等的,再去比較key值是否相同。
1 type typeAlg struct { 2 // function for hashing objects of this type 3 // (ptr to object, seed) -> hash 4 hash func(unsafe.Pointer, uintptr) uintptr 5 // function for comparing objects of this type 6 // (ptr to object A, ptr to object B) -> ==? 7 equal func(unsafe.Pointer, unsafe.Pointer) bool 8 9 // tophash calculates the tophash value for hash. 10 func tophash(hash uintptr) uint8 { 11 top := uint8(hash >> (sys.PtrSize*8 - 8)) 12 if top < minTopHash { 13 top += minTopHash 14 } 15 return top 16 }
golang爲每一個類型定義了類型描述器_type,並實現了hashable類型的_type.alg.hash和_type.alg.equal,以支持map的範型,定義了這類key用什麼hash函數、bucket的大小、怎麼比較之類的,經過這個變量來實現範型。
1 //makemap爲make(map [k] v,hint)實現Go map建立。 2 //若是編譯器已肯定映射或第一個存儲桶,能夠在堆棧上建立,hmap或bucket能夠爲非nil。 3 //若是h!= nil,則能夠直接在h中建立map。 4 //若是h.buckets!= nil,則指向的存儲桶能夠用做第一個存儲桶。 5 func makemap(t *maptype, hint int, h *hmap) *hmap { 6 if hint < 0 || hint > int(maxSliceCap(t.bucket.size)) { 7 hint = 0 8 } 9 10 // 初始化Hmap 11 if h == nil { 12 h = new(hmap) 13 } 14 h.hash0 = fastrand() 15 16 // 查找將保存請求的元素數的size參數 17 B := uint8(0) 18 for overLoadFactor(hint, B) { 19 B++ 20 } 21 h.B = B 22 23 // 分配初始哈希表 24 // if B == 0, 稍後會延遲分配buckets字段(在mapassign中) 25 //若是提示很大,則將內存清零可能須要一段時間。 26 if h.B != 0 { 27 var nextOverflow *bmap 28 h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) 29 if nextOverflow != nil { 30 h.extra = new(mapextra) 31 h.extra.nextOverflow = nextOverflow 32 } 33 } 34 35 return h 36 }
1 // makeBucketArray初始化地圖存儲區的後備數組。 2 // 1 << b是要分配的最小存儲桶數。 3 // dirtyalloc以前應該爲nil或bucket數組 4 //由makeBucketArray使用相同的t和b參數分配。 5 //若是dirtyalloc爲零,則將分配一個新的支持數組,dirtyalloc將被清除並做爲後備數組重用。 6 func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) { 7 base := bucketShift(b) 8 nbuckets := base 9 // 對於小b,溢出桶不太可能出現。 10 // 避免計算的開銷。 11 if b >= 4 { 12 //加上估計的溢出桶數 13 //插入元素的中位數 14 //與此值b一塊兒使用。 15 nbuckets += bucketShift(b - 4) 16 sz := t.bucket.size * nbuckets 17 up := roundupsize(sz) 18 if up != sz { 19 nbuckets = up / t.bucket.size 20 } 21 } 22 if dirtyalloc == nil { 23 buckets = newarray(t.bucket, int(nbuckets)) 24 } else { 25 // dirtyalloc先前是由上面的newarray(t.bucket,int(nbuckets)),但不能爲空。 26 buckets = dirtyalloc 27 size := t.bucket.size * nbuckets 28 if t.bucket.kind&kindNoPointers == 0 { 29 memclrHasPointers(buckets, size) 30 } else { 31 memclrNoHeapPointers(buckets, size) 32 } 33 } 34 35 if base != nbuckets { 36 //咱們預先分配了一些溢出桶。 37 //爲了將跟蹤這些溢出桶的開銷降至最低,咱們使用的約定是,若是預分配的溢出存儲桶發生了溢出指針爲零,則經過碰撞指針還有更多可用空間。 38 //對於最後一個溢出存儲區,咱們須要一個安全的非nil指針;只是用bucket。 39 nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) 40 last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) 41 last.setoverflow(t, (*bmap)(buckets)) 42 } 43 return buckets, nextOverflow 44 }
1 // mapaccess1返回指向h [key]的指針。從不返回nil,而是 若是值類型爲零,它將返回對零對象的引用,該鍵不在map中。 2 //注意:返回的指針可能會使整個map保持活動狀態,所以請不要堅持很長時間。 3 func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { 4 if raceenabled && h != nil { //raceenabled是否啓用數據競爭檢測。 5 callerpc := getcallerpc() 6 pc := funcPC(mapaccess1) 7 racereadpc(unsafe.Pointer(h), callerpc, pc) 8 raceReadObjectPC(t.key, key, callerpc, pc) 9 } 10 if msanenabled && h != nil { 11 msanread(key, t.key.size) 12 } 13 if h == nil || h.count == 0 { 14 return unsafe.Pointer(&zeroVal[0]) 15 } 16 // 併發訪問檢查 17 if h.flags&hashWriting != 0 { 18 throw("concurrent map read and map write") 19 } 20 21 // 計算key的hash值 22 alg := t.key.alg 23 hash := alg.hash(key, uintptr(h.hash0)) // alg.hash 24 25 // hash值對m取餘數獲得對應的bucket 26 m := uintptr(1)<<h.B - 1 27 b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) 28 29 // 若是老的bucket尚未遷移,則在老的bucket裏面找 30 if c := h.oldbuckets; c != nil { 31 if !h.sameSizeGrow() { 32 m >>= 1 33 } 34 oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize))) 35 if !evacuated(oldb) { 36 b = oldb 37 } 38 } 39 40 // 計算tophash,取高8位 41 top := uint8(hash >> (sys.PtrSize*8 - 8)) 42 43 for { 44 for i := uintptr(0); i < bucketCnt; i++ { 45 // 檢查top值,如高8位不同就找下一個 46 if b.tophash[i] != top { 47 continue 48 } 49 50 // 取key的地址 51 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 52 53 if alg.equal(key, k) { // alg.equal 54 // 取value得地址 55 v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 56 } 57 } 58 59 // 若是當前bucket沒有找到,則找bucket鏈的下一個bucket 60 b = b.overflow(t) 61 if b == nil { 62 // 返回零值 63 return unsafe.Pointer(&zeroVal[0]) 64 } 65 } 66 }
先定位出bucket,若是正在擴容,而且這個bucket還沒搬到新的hash表中,那麼就從老的hash表中查找。
在bucket中進行順序查找,使用高八位進行快速過濾,高八位相等,再比較key是否相等,找到就返回value。若是當前bucket找不到,就往下找overflow bucket,都沒有就返回零值。
訪問的時候,並不進行擴容的數據搬遷。而且併發有寫操做時拋異常。
注意,t.bucketsize並非bmap的size,而是bmap加上存儲key、value、overflow指針,因此查找bucket的時候時候用的不是bmap的szie。
1 // 與mapaccess相似,可是若是map中不存在密鑰,則爲該密鑰分配一個插槽 2 func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { 3 ... 4 //設置hashWriting調用alg.hash,由於alg.hash可能出現緊急狀況後,在這種狀況下,咱們實際上並無進行寫操做. 5 h.flags |= hashWriting 6 7 if h.buckets == nil { 8 h.buckets = newobject(t.bucket) // newarray(t.bucket, 1) 9 } 10 11 again: 12 bucket := hash & bucketMask(h.B) 13 if h.growing() { 14 growWork(t, h, bucket) 15 } 16 b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize))) 17 top := tophash(hash) 18 19 var inserti *uint8 20 var insertk unsafe.Pointer 21 var val unsafe.Pointer 22 for { 23 for i := uintptr(0); i < bucketCnt; i++ { 24 if b.tophash[i] != top { 25 if b.tophash[i] == empty && inserti == nil { 26 inserti = &b.tophash[i] 27 insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 28 val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 29 } 30 continue 31 } 32 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 33 if t.indirectkey { 34 k = *((*unsafe.Pointer)(k)) 35 } 36 if !alg.equal(key, k) { 37 continue 38 } 39 // 已經有一個 mapping for key. 更新它. 40 if t.needkeyupdate { 41 typedmemmove(t.key, k, key) 42 } 43 val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 44 goto done 45 } 46 ovf := b.overflow(t) 47 if ovf == nil { 48 break 49 } 50 b = ovf 51 } 52 //// 若是已經達到了load factor的最大值,就繼續擴容。 53 //找不到鍵的映射。分配新單元格並添加條目。 54 //若是達到最大負載係數或溢出桶過多,而且咱們尚未處於成長的中間,就開始擴容。 55 if !h.growing() && (overLoadFactor(h.count+1, h.B) || 56 tooManyOverflowBuckets(h.noverflow, h.B)) { 57 hashGrow(t, h) 58 goto again // //擴大表格會使全部內容無效, so try again 59 } 60 if inserti == nil { 61 // 當前全部存儲桶已滿,請分配一個新的存儲桶 62 newb := h.newoverflow(t, b) 63 inserti = &newb.tophash[0] 64 insertk = add(unsafe.Pointer(newb), dataOffset) 65 val = add(insertk, bucketCnt*uintptr(t.keysize)) 66 } 67 68 // 在插入的位置,存儲鍵值 69 if t.indirectkey { 70 kmem := newobject(t.key) 71 *(*unsafe.Pointer)(insertk) = kmem 72 insertk = kmem 73 } 74 if t.indirectvalue { 75 vmem := newobject(t.elem) 76 *(*unsafe.Pointer)(val) = vmem 77 } 78 typedmemmove(t.key, insertk, key) 79 *inserti = top 80 h.count++ 81 82 done: 83 if h.flags&hashWriting == 0 { 84 throw("concurrent map writes") 85 } 86 h.flags &^= hashWriting 87 if t.indirectvalue { 88 val = *((*unsafe.Pointer)(val)) 89 } 90 return val 91 }
hash表若是正在擴容,而且此次要操做的bucket還沒搬到新hash表中,那麼先進行搬遷(擴容細節下面細說)。
在buck中尋找key,同時記錄下第一個空位置,若是找不到,那麼就在空位置中插入數據;若是找到了,那麼就更新對應的value;
找不到key就看下需不須要擴容,須要擴容而且沒有正在擴容,那麼就進行擴容,而後回到第一步。
找不到key,不須要擴容,可是沒有空slot,那麼就分配一個overflow bucket掛在鏈表結尾,用新bucket的第一個slot放存放數據。
1 func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) { 2 ... 3 // Set hashWriting after calling alg.hash, since alg.hash may panic, 4 // in which case we have not actually done a write (delete). 5 h.flags |= hashWriting 6 7 bucket := hash & bucketMask(h.B) 8 if h.growing() { 9 growWork(t, h, bucket) 10 } 11 b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) 12 top := tophash(hash) 13 search: 14 for ; b != nil; b = b.overflow(t) { 15 for i := uintptr(0); i < bucketCnt; i++ { 16 if b.tophash[i] != top { 17 continue 18 } 19 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) 20 k2 := k 21 if t.indirectkey { 22 k2 = *((*unsafe.Pointer)(k2)) 23 } 24 if !alg.equal(key, k2) { 25 continue 26 } 27 // 若是其中有指針,則僅清除鍵。 28 if t.indirectkey { 29 *(*unsafe.Pointer)(k) = nil 30 } else if t.key.kind&kindNoPointers == 0 { 31 memclrHasPointers(k, t.key.size) 32 } 33 v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) 34 if t.indirectvalue { 35 *(*unsafe.Pointer)(v) = nil 36 } else if t.elem.kind&kindNoPointers == 0 { 37 memclrHasPointers(v, t.elem.size) 38 } else { 39 memclrNoHeapPointers(v, t.elem.size) 40 } 41 // 若找到把對應的tophash裏面的打上空的標記 42 b.tophash[i] = empty 43 h.count-- 44 break search 45 } 46 } 47 48 if h.flags&hashWriting == 0 { 49 throw("concurrent map writes") 50 } 51 h.flags &^= hashWriting 52 }
若是正在擴容,而且操做的bucket還沒搬遷完,那麼搬遷bucket。
找出對應的key,若是key、value是包含指針的那麼會清理指針指向的內存,不然不會回收內存。
經過上面的過程咱們知道了,插入、刪除過程都會觸發擴容,判斷擴容的函數以下:
1 // overLoadFactor 判斷放置在1 << B個存儲桶中的計數項目是否超過loadFactor。 2 func overLoadFactor(count int, B uint8) bool { 3 return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen) 4 //return 元素個數>8 && count>bucket數量*6.5,其中loadFactorNum是常量13,loadFactorDen是常量2,因此是6.5,bucket數量不算overflow bucket. 5 } 6 7 // tooManyOverflowBuckets 判斷noverflow存儲桶對於1 << B存儲桶的map是否過多。 8 // 請注意,大多數這些溢出桶必須稀疏使用。若是使用密集,則咱們已經觸發了常規map擴容。 9 func tooManyOverflowBuckets(noverflow uint16, B uint8) bool { 10 // 若是閾值過低,咱們會作多餘的工做。若是閾值過高,則增大和縮小的映射可能會保留大量未使用的內存。 11 //「太多」意味着(大約)溢出桶與常規桶同樣多。有關更多詳細信息,請參見incrnoverflow。 12 if B > 15 { 13 B = 15 14 } 15 // 譯器在這裏看不到B <16;掩碼B生成較短的移位碼。 16 return noverflow >= uint16(1)<<(B&15) 17 } 18 19 { 20 .... 21 // 若是咱們達到最大負載率或溢流桶過多,而且咱們尚未處於成長的中間,就開始成長。 22 if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) { 23 hashGrow(t, h) 24 goto again // 擴大表格會使全部內容失效,so try again 25 } 26 //if (不是正在擴容 && (元素個數/bucket數超過某個值 || 太多overflow bucket)) { 27 進行擴容 28 //} 29 .... 30 }
每次map進行更新或者新增的時候,會先經過以上函數判斷一下load factor。來決定是否擴容。若是須要擴容,那麼第一步須要作的,就是對hash表進行擴容:
1 //僅對hash表進行擴容,這裏不進行搬遷 2 func hashGrow(t *maptype, h *hmap) { 3 // 若是達到負載係數,則增大尺寸。不然,溢出bucket過多,所以,保持相同數量的存儲桶並橫向「增加」。 4 bigger := uint8(1) 5 if !overLoadFactor(h.count+1, h.B) { 6 bigger = 0 7 h.flags |= sameSizeGrow 8 } 9 oldbuckets := h.buckets 10 newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil) 11 12 flags := h.flags &^ (iterator | oldIterator) 13 if h.flags&iterator != 0 { 14 flags |= oldIterator 15 } 16 // 提交增加(atomic wrt gc) 17 h.B += bigger 18 h.flags = flags 19 h.oldbuckets = oldbuckets 20 h.buckets = newbuckets 21 h.nevacuate = 0 22 h.noverflow = 0 23 24 if h.extra != nil && h.extra.overflow != nil { 25 // 將當前的溢出bucket提高到老一代。 26 if h.extra.oldoverflow != nil { 27 throw("oldoverflow is not nil") 28 } 29 h.extra.oldoverflow = h.extra.overflow 30 h.extra.overflow = nil 31 } 32 if nextOverflow != nil { 33 if h.extra == nil { 34 h.extra = new(mapextra) 35 } 36 h.extra.nextOverflow = nextOverflow 37 } 38 39 //哈希表數據的實際複製是增量完成的,經過growWork()和evacuate()。 40 }
若是以前爲2^n ,那麼下一次擴容是2^(n+1),每次擴容都是以前的兩倍。擴容後須要從新計算每一項在hash中的位置,新表爲老的兩倍,此時前文的oldbacket用上了,用來存同時存在的兩個新舊map,等數據遷移完畢就能夠釋放oldbacket了。擴容的函數hashGrow其實僅僅是進行一些空間分配,字段的初始化,實際的搬遷操做是在growWork函數中:
1 func growWork(t *maptype, h *hmap, bucket uintptr) { 2 //確保咱們遷移了了對應的oldbucket,到咱們將要使用的存儲桶。 3 evacuate(t, h, bucket&h.oldbucketmask()) 4 5 // 疏散一箇舊桶以在生長上取得進展 6 if h.growing() { 7 evacuate(t, h, h.nevacuate) 8 } 9 }
優勢:均攤擴容時間,必定程度上縮短了擴容時間(和gc的引用計數法相似,都是均攤)overLoadFactor函數中有一個常量6.5(loadFactorNum/loadFactorDen)來進行影響擴容時機。這個值的來源是測試取中的結果。
map的併發操做不是安全的。併發起兩個goroutine,分別對map進行數據的增長:
1 func main() { 2 test := map[int]int {1:1} 3 go func() { 4 i := 0 5 for i < 10000 { 6 test[1]=1 7 i++ 8 } 9 }() 10 11 go func() { 12 i := 0 13 for i < 10000 { 14 test[1]=1 15 i++ 16 } 17 }() 18 19 time.Sleep(2*time.Second) 20 fmt.Println(test) 21 } 22 23 //fatal error: concurrent map read and map write
併發讀寫map結構的數據引發了錯誤。
解決方案1:加鎖
1 func main() { 2 test := map[int]int {1:1} 3 var s sync.RWMutex 4 go func() { 5 i := 0 6 for i < 10000 { 7 s.Lock() 8 test[1]=1 9 s.Unlock() 10 i++ 11 } 12 }() 13 14 go func() { 15 i := 0 16 for i < 10000 { 17 s.Lock() 18 test[1]=1 19 s.Unlock() 20 i++ 21 } 22 }() 23 24 time.Sleep(2*time.Second) 25 fmt.Println(test) 26 }
特色:實現簡單粗暴,好理解。可是鎖的粒度爲整個map,存在優化空間。適用場景:all。
解決方案2:sync.Map
1 func main() { 2 test := sync.Map{} 3 test.Store(1, 1) 4 go func() { 5 i := 0 6 for i < 10000 { 7 test.Store(1, 1) 8 i++ 9 } 10 }() 11 12 go func() { 13 i := 0 14 for i < 10000 { 15 test.Store(1, 1) 16 i++ 17 } 18 }() 19 20 time.Sleep(time.Second) 21 fmt.Println(test.Load(1)) 22 }
sync.Map的原理:sync.Map裏頭有兩個map一個是專門用於讀的read map,另外一個是纔是提供讀寫的dirty map;優先讀read map,若不存在則加鎖穿透讀dirty map,同時記錄一個未從read map讀到的計數,當計數到達必定值,就將read map用dirty map進行覆蓋。
特色:官方出品,經過空間換時間的方式,讀寫分離;不適用於大量寫的場景,會致使read map讀不到數據而進一步加鎖讀取,同時dirty map也會一直晉升爲read map,總體性能較差。適用場景:大量讀,少許寫。
解決方案3:分段鎖
這也是數據庫經常使用的方法,分段鎖每個讀寫鎖保護一段區間。sync.Map其實也是至關於表級鎖,只不過多讀寫分了兩個map,本質仍是同樣的。
優化方向:將鎖的粒度儘量下降來提升運行速度。思路:對一個大map進行hash,其內部是n個小map,根據key來來hash肯定在具體的那個小map中,這樣加鎖的粒度就變成1/n了。例如
golang裏的map是隻增不減的一種數組結構,他只會在刪除的時候進行打標記說明該內存空間已經empty了,不會回收。
1 var intMap map[int]int 2 3 func main() { 4 printMemStats("初始化") 5 6 // 添加1w個map值 7 intMap = make(map[int]int, 10000) 8 for i := 0; i < 10000; i++ { 9 intMap[i] = i 10 } 11 12 // 手動進行gc操做 13 runtime.GC() 14 // 再次查看數據 15 printMemStats("增長map數據後") 16 17 log.Println("刪除前數組長度:", len(intMap)) 18 for i := 0; i < 10000; i++ { 19 delete(intMap, i) 20 } 21 log.Println("刪除後數組長度:", len(intMap)) 22 23 // 再次進行手動GC回收 24 runtime.GC() 25 printMemStats("刪除map數據後") 26 27 // 設置爲nil進行回收 28 intMap = nil 29 runtime.GC() 30 printMemStats("設置爲nil後") 31 } 32 33 func printMemStats(mag string) { 34 var m runtime.MemStats 35 runtime.ReadMemStats(&m) 36 log.Printf("%v:分配的內存 = %vKB, GC的次數 = %v\n", mag, m.Alloc/1024, m.NumGC) 37 } 38 39 //初始化:分配的內存 = 65KB, GC的次數 = 0 40 //增長map數據後:分配的內存 = 381KB, GC的次數 = 1 41 //刪除前數組長度: 10000 42 //刪除後數組長度: 0 43 //刪除map數據後:分配的內存 = 381KB, GC的次數 = 2 44 //設置爲nil後:分配的內存 = 68KB, GC的次數 = 3
能夠看到delete是不會真正的把map釋放的,因此要回收map仍是須要設爲nil
sync.Map的原理詳解:https://www.jianshu.com/p/ec51dac3c65b 由淺入深sync.Map