超超艱難的回答完了面試官關於GMP相關問題,下面進入到了單例相關問題。單例雖然簡單,但是面試官也是層層深入,讓超超滿頭大汗,下面來看看單例面試官都問了些什麼吧。
認識單例
面試官:你知道mac中的回收站只能單開,但是訪達視窗可以多開吧?
考點:單例的使用場景優缺點
超超:知道呀,這應該是單例模式。我們日常工作中並沒有使用倆個廢紙簍的必要性,且廢紙簍之間的資源是共享,沒有必要多開浪費系統資源。
單例怎麼用
面試官:你剛才說到了單例,你知道go裡面怎麼使用單例嗎?
考點:sync.Once使用
超超:這個簡單,舉個例子,平時我們在構建專案時,因為配置資訊在全域性是資源共享的,所以會將讀取配置資訊的物件做成一個單例。
package main
import (
"fmt"
"sync"
)
//假設配置資訊中只有伺服器id
type Config struct {
id int
}
var (
once sync.Once
config *Config
)
func (p *Config) GetID() int {
return p.id
}
func getConfig() *Config {
//底層是倆個鎖,防止f沒執行完,其他new因未獲取到鎖,直接返回物件
once.Do(func() {
config = new(Config)
config.id = 1
fmt.Println("new config")
})
fmt.Println("get config")
return config
}
func main() {
for i := 0; i < 3; i++ {
_ = getConfig()
}
}
結果
new config
get config
get config
get config
原始碼實現
面試官:那你知道sync.Once的底層結構是什麼嗎?
考點:sync.Once
原始碼
超超:sync.Once
是由Once
結構體和Do
,doSlow
倆個方法實現的
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/x86),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
done
是標識位,用於判斷方法f
是否被執行完,done
的初始值為0,當f
執行結束時,done
被設為1。
m
做競態控制,當f
第一次執行還未結束時,通過m
加鎖的方式阻塞其他once.Do
執行f
。
這裡有個地方需要特別注意下,once.Do
是不可以巢狀使用的,巢狀使用將導致死鎖。
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
Do()
方法
作用:通過原子操作判斷o.done
,如果o.done==0
則f
未被執行完,進入doSlow(f func())
,如果f
執行結束則退出Do()
。
入參:無
出參:無
doSlow(f func())
方法
作用:通過加鎖的方式執行f
,並在f
執行結束時,將o.done
置為1
入參:執行體f
,通常為物件的建立或者模組資料載入
出參:無
面試官:很好,那你知道atomic.CompareAndSwapUint32(&o.done, 0, 1)
的作用是什麼嗎?
考點:sync包瞭解廣度
超超:CompareAndSwapUint32
簡稱CAS,通過原子操作判斷當o.done
值等於0時,使o.done
等於1並返回true
,當o.done
值不等於0,直接返回false
面試官:那你能說說Do()
方法中可以把atomic.LoadUint32
直接替換為atomic.CompareAndSwapUint32
嗎?
考點:多執行緒思維
超超:這個是不可以的,因為f
的執行是需要時間的,如果用CAS可能會導致f
建立的物件尚未完成,程式其他地方就可以呼叫f。如圖所示,A,B倆個協程都呼叫Once.Do
方法,A協程先完成CAS將done
值置為了1,導致B協程誤以為物件建立完成,繼而呼叫物件方法而出錯。
這裡doSlow
中的o.done == 0
判斷,也需要注意一下,因為可能會出現A,B倆個協程都進行了LoadUint32
判斷,並且都是true
,如果不進行第二次校驗的話,物件會被new倆次
擴充
面試官:看來你對原始碼sync.Once
的實現還比較熟悉,那你知道懶漢模式和餓漢模式嗎?
考點:建立單例的方式
超超:
餓漢模式:是指在程式啟動時就進行資料載入,這樣避免了資料衝突,也是執行緒安全的,但是這可能會造成記憶體浪費。比如在程式啟動時就構建Config物件,載入配置資訊,但是如果全域性都沒有用到Config物件,就會造成記憶體浪費。
懶漢模式:是指程式需要config物件時,再主動去載入資料,這樣做可以避免記憶體的浪費,比如當需要呼叫Config物件獲取資料時,再去new一個Config物件,然後通過物件獲取配置相關資訊。
面試官:看來對這個研究過哈。那我們來看這樣一個問題,你看廢紙簍裡面有圖片,資料夾,app等各種檔案,把這個抽象成各種不同型別的資料,你會用什麼容器去儲存他?
超超:這個我想一下(:為什麼我要給他說我用的是mac?
未完待續 ~
歡迎關注公眾號「Golang面試寶典」檢視文章最新進展,如果你有什麼問題想問超超,也歡迎新增我的微信,進讀者群和超超一起討論呀!
本作品採用《CC 協議》,轉載必須註明作者和本文連結