同學你的單例,夠面試嗎?

ssdlh發表於2021-05-20

超超艱難的回答完了面試官關於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結構體和DodoSlow倆個方法實現的

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==0f未被執行完,進入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 協議》,轉載必須註明作者和本文連結

相關文章