一般咱們去面試確定會有些不錯的Golang的面試題目的,因此總結下,讓其餘Golang開發者也能夠查看到,同時也用來檢測本身的能力和提醒本身的不足之處,歡迎你們補充和提交新的面試題目.php
Golang面試問題彙總:html
Golang中Goroutine 能夠經過 Channel 進行安全讀寫共享變量。java
ch := make(chan int) 無緩衝的channel因爲沒有緩衝發送和接收須要同步. ch := make(chan int, 2) 有緩衝channel不要求發送和接收操做同步.
CSP模型是上個世紀七十年代提出的,不一樣於傳統的多線程經過共享內存來通訊,CSP講究的是「以通訊的方式來共享內存」。用於描述兩個獨立的併發實體經過共享的通信 channel(管道)進行通訊的併發模型。 CSP中channel是第一類對象,它不關注發送消息的實體,而關注與發送消息時使用的channel。node
Golang中channel 是被單首創建而且能夠在進程之間傳遞,它的通訊模式相似於 boss-worker 模式的,一個實體經過將消息發送到channel 中,而後又監聽這個 channel 的實體處理,兩個實體之間是匿名的,這個就實現實體中間的解耦,其中 channel 是同步的一個消息被髮送到 channel 中,最終是必定要被另外的實體消費掉的,在實現原理上其實相似一個阻塞的消息隊列。python
Goroutine 是Golang實際併發執行的實體,它底層是使用協程(coroutine)實現併發,coroutine是一種運行在用戶態的用戶線程,相似於 greenthread,go底層選擇使用coroutine的出發點是由於,它具備如下特色:mysql
Golang中的Goroutine的特性:linux
Golang內部有三個對象: P對象(processor) 表明上下文(或者能夠認爲是cpu),M(work thread)表明工做線程,G對象(goroutine).ios
正常狀況下一個cpu對象啓一個工做線程對象,線程去檢查並執行goroutine對象。碰到goroutine對象阻塞的時候,會啓動一個新的工做線程,以充分利用cpu資源。 全部有時候線程對象會比處理器對象多不少.c++
咱們用以下圖分別表示P、M、G:git
G(Goroutine) :咱們所說的協程,爲用戶級的輕量級線程,每一個Goroutine對象中的sched保存着其上下文信息.
M(Machine) :對內核級線程的封裝,數量對應真實的CPU數(真正幹活的對象).
P(Processor) :即爲G和M的調度對象,用來調度G和M之間的關聯關係,其數量可經過GOMAXPROCS()來設置,默認爲核心數.
在單核狀況下,全部Goroutine運行在同一個線程(M0)中,每個線程維護一個上下文(P),任什麼時候刻,一個上下文中只有一個Goroutine,其餘Goroutine在runqueue中等待。
一個Goroutine運行完本身的時間片後,讓出上下文,本身回到runqueue中(以下圖所示)。
當正在運行的G0阻塞的時候(能夠須要IO),會再建立一個線程(M1),P轉到新的線程中去運行。
當M0返回時,它會嘗試從其餘線程中「偷」一個上下文過來,若是沒有偷到,會把Goroutine放到Global runqueue中去,而後把本身放入線程緩存中。 上下文會定時檢查Global runqueue。
Golang是爲併發而生的語言,Go語言是爲數很少的在語言層面實現併發的語言;也正是Go語言的併發特性,吸引了全球無數的開發者。
Golang的CSP併發模型,是經過Goroutine和Channel來實現的。
Goroutine 是Go語言中併發的執行單位。有點抽象,其實就是和傳統概念上的」線程「相似,能夠理解爲」線程「。 Channel是Go語言中各個併發結構體(Goroutine)以前的通訊機制。一般Channel,是各個Goroutine之間通訊的」管道「,有點相似於Linux中的管道。
通訊機制channel也很方便,傳數據用channel <- data,取數據用<-channel。
在通訊過程當中,傳數據channel <- data和取數據<-channel必然會成對出現,由於這邊傳,那邊取,兩個goroutine之間纔會實現通訊。
並且無論傳仍是取,必阻塞,直到另外的goroutine傳或者取爲止。
Golang 中經常使用的併發模型有三種:
無緩衝的通道指的是通道的大小爲0,也就是說,這種類型的通道在接收前沒有能力保存任何值,它要求發送 goroutine 和接收 goroutine 同時準備好,才能夠完成發送和接收操做。
從上面無緩衝的通道定義來看,發送 goroutine 和接收 gouroutine 必須是同步的,同時準備後,若是沒有同時準備好的話,先執行的操做就會阻塞等待,直到另外一個相對應的操做準備好爲止。這種無緩衝的通道咱們也稱之爲同步通道。
func main() { ch := make(chan struct{}) go func() { fmt.Println("start working") time.Sleep(time.Second * 1) ch <- struct{}{} }() <-ch fmt.Println("finished") }
當主 goroutine 運行到 <-ch 接受 channel 的值的時候,若是該 channel 中沒有數據,就會一直阻塞等待,直到有值。 這樣就能夠簡單實現併發控制
Goroutine是異步執行的,有的時候爲了防止在結束mian函數的時候結束掉Goroutine,因此須要同步等待,這個時候就須要用 WaitGroup了,在 sync 包中,提供了 WaitGroup ,它會等待它收集的全部 goroutine 任務所有完成。在WaitGroup裏主要有三個方法:
在主 goroutine 中 Add(delta int) 索要等待goroutine 的數量。 在每個 goroutine 完成後 Done() 表示這一個goroutine 已經完成,當全部的 goroutine 都完成後,在主 goroutine 中 WaitGroup 返回返回。
func main(){ var wg sync.WaitGroup var urls = []string{ "http://www.golang.org/", "http://www.google.com/", } for _, url := range urls { wg.Add(1) go func(url string) { defer wg.Done() http.Get(url) }(url) } wg.Wait() }
在Golang官網中對於WaitGroup介紹是A WaitGroup must not be copied after first use
,在 WaitGroup 第一次使用後,不能被拷貝
應用示例:
func main(){ wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Printf("i:%d", i) wg.Done() }(wg, i) } wg.Wait() fmt.Println("exit") }
運行:
i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]: sync.runtime_Semacquire(0xc000094018) /home/keke/soft/go/src/runtime/sema.go:56 +0x39 sync.(*WaitGroup).Wait(0xc000094010) /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64 main.main() /home/keke/go/Test/wait.go:17 +0xab exit status 2
它提示全部的 goroutine 都已經睡眠了,出現了死鎖。這是由於 wg 給拷貝傳遞到了 goroutine 中,致使只有 Add 操做,其實 Done操做是在 wg 的副本執行的。
所以 Wait 就死鎖了。
這個第一個修改方式:將匿名函數中 wg 的傳入類型改成 *sync.WaitGrou,這樣就能引用到正確的WaitGroup了。 這個第二個修改方式:將匿名函數中的 wg 的傳入參數去掉,由於Go支持閉包類型,在匿名函數中能夠直接使用外面的 wg 變量
一般,在一些簡單場景下使用 channel 和 WaitGroup 已經足夠了,可是當面臨一些複雜多變的網絡併發場景下 channel 和 WaitGroup 顯得有些力不從心了。 好比一個網絡請求 Request,每一個 Request 都須要開啓一個 goroutine 作一些事情,這些 goroutine 又可能會開啓其餘的 goroutine,好比數據庫和RPC服務。 因此咱們須要一種能夠跟蹤 goroutine 的方案,才能夠達到控制他們的目的,這就是Go語言爲咱們提供的 Context,稱之爲上下文很是貼切,它就是goroutine 的上下文。 它是包括一個程序的運行環境、現場和快照等。每一個程序要運行時,都須要知道當前程序的運行狀態,一般Go 將這些封裝在一個 Context 裏,再將它傳給要執行的 goroutine 。
context 包主要是用來處理多個 goroutine 之間共享數據,及多個 goroutine 的管理。
context 包的核心是 struct Context,接口聲明以下:
// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }
Done() 返回一個只能接受數據的channel類型,當該context關閉或者超時時間到了的時候,該channel就會有一個取消信號
Err() 在Done() 以後,返回context 取消的緣由。
Deadline() 設置該context cancel的時間點
Value() 方法容許 Context 對象攜帶request做用域的數據,該數據必須是線程安全的。
Context 對象是線程安全的,你能夠把一個 Context 對象傳遞給任意個數的 gorotuine,對它執行 取消 操做時,全部 goroutine 都會接收到取消信號。
一個 Context 不能擁有 Cancel 方法,同時咱們也只能 Done channel 接收數據。 其中的緣由是一致的:接收取消信號的函數和發送信號的函數一般不是一個。 典型的場景是:父操做爲子操做操做啓動 goroutine,子操做也就不能取消父操做。
首先JSON 標準庫對 nil slice 和 空 slice 的處理是不一致.
一般錯誤的用法,會報數組越界的錯誤,由於只是聲明瞭slice,卻沒有給實例化的對象。
var slice []int slice[1] = 0
此時slice的值是nil,這種狀況能夠用於須要返回slice的函數,當函數出現異常的時候,保證函數依然會有nil的返回值。
empty slice 是指slice不爲nil,可是slice沒有值,slice的底層的空間是空的,此時的定義以下:
slice := make([]int,0) slice := []int{}
當咱們查詢或者處理一個空的列表的時候,這很是有用,它會告訴咱們返回的是一個列表,可是列表內沒有任何值。
總之,nil slice 和 empty slice是不一樣的東西,須要咱們加以區分的.
進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。每一個進程都有本身的獨立內存空間,不一樣進程經過進程間通訊來通訊。因爲進程比較重量,佔據獨立的內存,因此上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大,但相對比較穩定安全。
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源。線程間通訊主要經過共享內存,上下文切換很快,資源開銷較少,但相比進程不夠穩定容易丟失數據。
協程是一種用戶態的輕量級線程,協程的調度徹底由用戶控制。協程擁有本身的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操做棧則基本沒有內核切換的開銷,能夠不加鎖的訪問全局變量,因此上下文的切換很是快。
互斥鎖就是互斥變量mutex,用來鎖住臨界區的.
條件鎖就是條件變量,當進程的某些資源要求不知足時就進入休眠,也就是鎖住了。當資源被分配到了,條件鎖打開,進程繼續運行;讀寫鎖,也相似,用於緩衝區等臨界資源能互斥訪問的。
一般有些公共數據修改的機會不多,但其讀的機會不少。而且在讀的過程當中會伴隨着查找,給這種代碼加鎖會下降咱們的程序效率。讀寫鎖能夠解決這個問題。
注意:寫獨佔,讀共享,寫鎖優先級高
通常狀況下,若是同一個線程前後兩次調用lock,在第二次調用時,因爲鎖已經被佔用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被本身佔用着的,該線程又被掛起而沒有機會釋放鎖,所以就永遠處於掛起等待狀態了,這叫作死鎖(Deadlock)。 另一種狀況是:若線程A得到了鎖1,線程B得到了鎖2,這時線程A調用lock試圖得到鎖2,結果是須要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖得到鎖1,結果是須要掛起等待線程A釋放鎖1,因而線程A和B都永遠處於掛起狀態了。
死鎖產生的四個必要條件:
a. 預防死鎖
能夠把資源一次性分配:(破壞請求和保持條件)
而後剝奪資源:即當某進程新的資源未知足時,釋放已佔有的資源(破壞不可剝奪條件)
資源有序分配法:系統給每類資源賦予一個編號,每個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)
b. 避免死鎖
預防死鎖的幾種策略,會嚴重地損害系統性能。所以在避免死鎖時,要施加較弱的限制,從而得到 較滿意的系統性能。因爲在避免死鎖的策略中,容許進程動態地申請資源。於是,系統在進行資源分配以前預先計算資源分配的安全性。若這次分配不會致使系統進入不安全狀態,則將資源分配給進程;不然,進程等待。其中最具備表明性的避免死鎖算法是銀行家算法。
c. 檢測死鎖
首先爲每一個進程和每一個資源指定一個惟一的號碼,而後創建資源分配表和進程等待表.
d. 解除死鎖
當發現有進程死鎖後,便應當即把它從死鎖狀態中解脫出來,常採用的方法有.
e. 剝奪資源
從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態.
f. 撤消進程
能夠直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除爲止.所謂代價是指優先級、運行代價、進程的重要性和價值等。
一般小對象過多會致使GC三色法消耗過多的GPU。優化思路是,減小對象分配.
同步訪問共享數據是處理數據競爭的一種有效的方法.golang在1.1以後引入了競爭檢測機制,能夠使用 go run -race 或者 go build -race來進行靜態檢測。 其在內部的實現是,開啓多個協程執行同一個命令, 而且記錄下每一個變量的狀態.
競爭檢測器基於C/C++的ThreadSanitizer 運行時庫,該庫在Google內部代碼基地和Chromium找到許多錯誤。這個技術在2012年九月集成到Go中,從那時開始,它已經在標準庫中檢測到42個競爭條件。如今,它已是咱們持續構建過程的一部分,當競爭條件出現時,它會繼續捕捉到這些錯誤。
競爭檢測器已經徹底集成到Go工具鏈中,僅僅添加-race標誌到命令行就使用了檢測器。
$ go test -race mypkg // 測試包 $ go run -race mysrc.go // 編譯和運行程序 $ go build -race mycmd // 構建程序 $ go install -race mypkg // 安裝程序
要想解決數據競爭的問題能夠使用互斥鎖sync.Mutex,解決數據競爭(Data race),也能夠使用管道解決,使用管道的效率要比互斥鎖高.
Channel是Go中的一個核心類型,能夠把它當作一個管道,經過它併發核心單元就能夠發送或者接收數據進行通信(communication),Channel也能夠理解是一個先進先出的隊列,經過管道進行通訊。
Golang的Channel,發送一個數據到Channel 和 從Channel接收一個數據 都是 原子性的。並且Go的設計思想就是:不要經過共享內存來通訊,而是經過通訊來共享內存,前者就是傳統的加鎖,後者就是Channel。也就是說,設計Channel的主要目的就是在多任務間傳遞數據的,這固然是安全的。
開發高性能網絡程序時,windows開發者們言必稱Iocp,linux開發者們則言必稱Epoll。你們都明白Epoll是一種IO多路複用技術,能夠很是高效的處理數以百萬計的Socket句柄,比起之前的Select和Poll效率提升了不少。
先簡單瞭解下如何使用C庫封裝的3個epoll系統調用。
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
使用起來很清晰,首先要調用epoll_create
創建一個epoll對象。參數size是內核保證可以正確處理的最大句柄數,多於這個最大數時內核可不保證效果。 epoll_ctl能夠操做上面創建的epoll,例如,將剛創建的socket
加入到epoll中讓其監控,或者把 epoll正在監控的某個socket句柄移出epoll,再也不監控它等等。
epoll_wait
在調用時,在給定的timeout時間內,當在監控的全部句柄中有事件發生時,就返回用戶態的進程。
從調用方式就能夠看到epoll相比select/poll的優越之處是,由於後者每次調用時都要傳遞你所要監控的全部socket給select/poll系統調用,這意味着須要將用戶態的socket列表copy到內核態,若是以萬計的句柄會致使每次都要copy幾十幾百KB的內存到內核態,很是低效。而咱們調用epoll_wait
時就至關於以往調用select/poll,可是這時卻不用傳遞socket句柄給內核,由於內核已經在epoll_ctl中拿到了要監控的句柄列表。
因此,實際上在你調用epoll_create
後,內核就已經在內核態開始準備幫你存儲要監控的句柄了,每次調用epoll_ctl
只是在往內核的數據結構裏塞入新的socket句柄。
在內核裏,一切皆文件。因此,epoll向內核註冊了一個文件系統,用於存儲上述的被監控socket。當你調用epoll_create時,就會在這個虛擬的epoll文件系統裏建立一個file結點。固然這個file不是普通文件,它只服務於epoll。
epoll在被內核初始化時(操做系統啓動),同時會開闢出epoll本身的內核高速cache區,用於安置每個咱們想監控的socket,這些socket會以紅黑樹的形式保存在內核cache裏,以支持快速的查找、插入、刪除。這個內核高速cache區,就是創建連續的物理內存頁,而後在之上創建slab層,一般來說,就是物理上分配好你想要的size的內存對象,每次使用時都是使用空閒的已分配好的對象。
static int __init eventpoll_init(void) { ... ... /* Allocates slab cache used to allocate "struct epitem" items */ epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); /* Allocates slab cache used to allocate "struct eppoll_entry" */ pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL); ... ... }
epoll的高效就在於,當咱們調用epoll_ctl
往裏塞入百萬個句柄時,epoll_wait
仍然能夠飛快的返回,並有效的將發生事件的句柄給咱們用戶。這是因爲咱們在調用epoll_create
時,內核除了幫咱們在epoll文件系統裏建了個file結點,在內核cache裏建了個紅黑樹用於存儲之後epoll_ctl傳來的socket外,還會再創建一個list鏈表,用於存儲準備就緒的事件,當epoll_wait調用時,僅僅觀察這個list鏈表裏有沒有數據便可。有數據就返回,沒有數據就sleep,等到timeout時間到後即便鏈表沒數據也返回。因此,epoll_wait很是高效。
並且,一般狀況下即便咱們要監控百萬計的句柄,大多一次也只返回不多量的準備就緒句柄而已,因此,epoll_wait僅須要從內核態copy少許的句柄到用戶態而已,所以就會很是的高效!
然而,這個準備就緒list鏈表是怎麼維護的呢?當咱們執行epoll_ctl時,除了把socket放到epoll文件系統裏file對象對應的紅黑樹上以外,還會給內核中斷處理程序註冊一個回調函數,告訴內核,若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。因此,當一個socket上有數據到了,內核在把網卡上的數據copy到內核中後就來把socket插入到準備就緒鏈表裏了。
如此,一個紅黑樹,一張準備就緒句柄鏈表,少許的內核cache,就幫咱們解決了大併發下的socket處理問題。執行epoll_create
時,建立了紅黑樹和就緒鏈表,執行epoll_ctl時,若是增長socket句柄,則檢查在紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。執行epoll_wait時馬上返回準備就緒鏈表裏的數據便可。
最後看看epoll獨有的兩種模式LT和ET。不管是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在之後調用epoll_wait時每次返回這個句柄,而ET模式僅在第一次返回。
當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時咱們調用epoll_wait
,會把準備就緒的socket拷貝到用戶態內存,而後清空準備就緒list鏈表,最後,epoll_wait
須要作的事情,就是檢查這些socket,若是不是ET模式(就是LT模式的句柄了),而且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。因此,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即便socket上的事件沒有處理完,也是不會每次從epoll_wait返回的。
所以epoll比select的提升其實是一個用空間換時間思想的具體應用.對比阻塞IO的處理模型, 能夠看到採用了多路複用IO以後, 程序能夠自由的進行本身除了IO操做以外的工做, 只有到IO狀態發生變化的時候由多路複用IO進行通知, 而後再採起相應的操做, 而不用一直阻塞等待IO狀態發生變化,提升效率.
首先咱們先來了解下垃圾回收.什麼是垃圾回收?
內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序開發者必須對內存當心的進行管理操做,控制內存的申請及釋放。由於稍有不慎,就可能產生內存泄露問題,這種問題不易發現而且難以定位,一直成爲困擾程序開發者的噩夢。如何解決這個頭疼的問題呢?
過去通常採用兩種辦法:
內存泄露檢測工具。這種工具的原理通常是靜態代碼掃描,經過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具不免有疏漏和不足,只能起到輔助做用。
智能指針。這是 c++ 中引入的自動內存管理方法,經過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存自動釋放的目的。這種方法是採用最普遍的作法,可是對程序開發者有必定的學習成本(並不是語言層面的原生支持),並且一旦有忘記使用的場景依然沒法避免內存泄露。
爲了解決這個問題,後來開發出來的幾乎全部新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而沒必要關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對再也不使用的內存資源進行自動回收的行爲就被稱爲垃圾回收。
經常使用的垃圾回收的方法:
這是最簡單的一種垃圾回收算法,和以前提到的智能指針殊途同歸。對每一個對象維護一個引用計數,當引用該對象的對象被銷燬或更新時被引用對象的引用計數自動減一,當被引用對象被建立或被賦值給其餘對象時引用計數自動加一。當引用計數爲0時則當即回收對象。
這種方法的優勢是實現簡單,而且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較普遍,如ios cocoa框架,php,python等。
可是簡單引用計數算法也有明顯的缺點:
一種簡單的解決方法就是編譯器將相鄰的引用計數更新操做合併到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時經過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有不少其餘方法,具體能夠參考這裏。
當對象間發生循環引用時引用鏈中的對象都沒法獲得釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用並主動打破循環鏈。固然這也增長了垃圾回收的複雜度。
標記-清除(mark and sweep)分爲兩步,標記從根變量開始迭代得遍歷全部被引用的對象,對可以經過應用遍歷訪問到的對象都進行標記爲「被引用」;標記完成後進行清除操做,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操做)。這種方法解決了引用計數的不足,可是也有比較明顯的問題:每次啓動垃圾回收都會暫停當前全部的正常代碼執行,回收是系統響應能力大大下降!固然後續也出現了不少mark&sweep算法的變種(如三色標記法)優化了這個問題。
java的jvm 就使用的分代回收的思路。在面向對象編程語言中,絕大多數對象的生命週期都很是短。分代收集的基本思想是,將堆劃分爲兩個或多個稱爲代(generation)的空間。新建立的對象存放在稱爲新生代(young generation)中(通常來講,新生代的大小會比 老年代小不少),隨着垃圾回收的重複執行,生命週期較長的對象會被提高(promotion)到老年代中(這裏用到了一個分類的思路,這個是也是科學思考的一個基本思路)。
所以,新生代垃圾回收和老年代垃圾回收兩種不一樣的垃圾回收方式應運而生,分別用於對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度很是快,比老年代快幾個數量級,即便新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是由於大多數對象的生命週期都很短,根本無需提高到老年代。
Golang GC 時會發生什麼?
Golang 1.5後,採起的是「非分代的、非移動的、併發的、三色的」標記清除垃圾回收算法。
golang 中的 gc 基本上是標記清除的過程:
gc的過程一共分爲四個階段:
整個進程空間裏申請每一個對象佔據的內存能夠視爲一個圖,初始狀態下每一個內存對象都是白色標記。
Golang gc 優化的核心就是儘可能使得 STW(Stop The World) 的時間愈來愈短。
詳細的Golang的GC介紹能夠參看Golang垃圾回收.
goroutine是Golang語言中最經典的設計,也是其魅力所在,goroutine的本質是協程,是實現並行計算的核心。 goroutine使用方式很是的簡單,只需使用go關鍵字便可啓動一個協程,而且它是處於異步方式運行,你不須要等它運行完成之後在執行之後的代碼。
go func()//經過go關鍵字啓動一個協程來運行函數
協程:
協程擁有本身的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。 所以,協程能保留上一次調用時的狀態(即全部局部狀態的一個特定組合),每次過程重入時,就至關於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。 線程和進程的操做是由程序觸發系統接口,最後的執行者是系統;協程的操做執行者則是用戶自身程序,goroutine也是協程。
groutine能擁有強大的併發實現是經過GPM調度模型實現.
Go的調度器內部有四個重要的結構:M,P,S,Sched,如上圖所示(Sched未給出).
調度實現:
從上圖中能夠看到,有2個物理線程M,每個M都擁有一個處理器P,每個也都有一個正在運行的goroutine。P的數量能夠經過GOMAXPROCS()來設置,它其實也就表明了真正的併發度,即有多少個goroutine能夠同時運行。
圖中灰色的那些goroutine並無運行,而是出於ready的就緒態,正在等待被調度。P維護着這個隊列(稱之爲runqueue),Go語言裏,啓動一個goroutine很容易:go function 就行,因此每有一個go語句被執行,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪一個goroutine?)一個goroutine執行。
當一個OS線程M0陷入阻塞時,P轉而在運行M1,圖中的M1多是正被建立,或者從線程緩存中取出。
當MO返回時,它必須嘗試取得一個P來運行goroutine,通常狀況下,它會從其餘的OS線程那裏拿一個P過來, 若是沒有拿到的話,它就把goroutine放在一個global runqueue裏,而後本身睡眠(放入線程緩存裏)。全部的P也會週期性的檢查global runqueue並運行其中的goroutine,不然global runqueue上的goroutine永遠沒法執行。
另外一種狀況是P所分配的任務G很快就執行完了(分配不均),這就致使了這個處理器P很忙,可是其餘的P還有任務,此時若是global runqueue沒有任務G了,那麼P不得不從其餘的P裏拿一些G來執行。
一般來講,若是P從其餘的P那裏要拿任務的話,通常就拿run queue的一半,這就確保了每一個OS線程都能充分的使用。
並行是指兩個或者多個事件在同一時刻發生;併發是指兩個或多個事件在同一時間間隔發生。
並行是在不一樣實體上的多個事件,併發是在同一實體上的多個事件。在一臺處理器上「同時」處理多個任務,在多臺處理器上同時處理多個任務。如hadoop分佈式集羣
併發偏重於多個任務交替執行,而多個任務之間有可能仍是串行的。而並行是真正意義上的「同時執行」。
併發編程是指在一臺處理器上「同時」處理多個任務。併發是在同一實體上的多個事件。多個事件在同一時間間隔發生。併發編程的目標是充分的利用處理器的每個核,以達到最高的處理性能。
負載均衡Load Balance)是高可用網絡基礎架構的關鍵組件,一般用於將工做負載分佈到多個服務器來提升網站、應用、數據庫或其餘服務的性能和可靠性。負載均衡,其核心就是網絡流量分發,分不少維度。
負載均衡(Load Balance)一般是分攤到多個操做單元上進行執行,例如Web服務器、FTP服務器、企業關鍵應用服務器和其它關鍵任務服務器等,從而共同完成工做任務。
負載均衡是創建在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增長吞吐量、增強網絡數據處理能力、提升網絡的靈活性和可用性。
經過一個例子詳細介紹:
在這裏用戶是直連到 web 服務器,若是這個服務器宕機了,那麼用戶天然也就沒辦法訪問了。 另外,若是同時有不少用戶試圖訪問服務器,超過了其能處理的極限,就會出現加載速度緩慢或根本沒法鏈接的狀況。
而經過在後端引入一個負載均衡器和至少一個額外的 web 服務器,能夠緩解這個故障。 一般狀況下,全部的後端服務器會保證提供相同的內容,以便用戶不管哪一個服務器響應,都能收到一致的內容。
用戶訪問負載均衡器,再由負載均衡器將請求轉發給後端服務器。在這種狀況下,單點故障如今轉移到負載均衡器上了。 這裏又能夠經過引入第二個負載均衡器來緩解。
那麼負載均衡器的工做方式是什麼樣的呢,負載均衡器又能夠處理什麼樣的請求?
負載均衡器的管理員能主要爲下面四種主要類型的請求設置轉發規則:
負載均衡器如何選擇要轉發的後端服務器?
負載均衡器通常根據兩個因素來決定要將請求轉發到哪一個服務器。首先,確保所選擇的服務器可以對請求作出響應,而後根據預先配置的規則從健康服務器池(healthy pool)中進行選擇。
由於,負載均衡器應當只選擇能正常作出響應的後端服務器,所以就須要有一種判斷後端服務器是否健康的方法。爲了監視後臺服務器的運行情況,運行狀態檢查服務會按期嘗試使用轉發規則定義的協議和端口去鏈接後端服務器。 若是,服務器沒法經過健康檢查,就會從池中剔除,保證流量不會被轉發到該服務器,直到其再次經過健康檢查爲止。
負載均衡算法
負載均衡算法決定了後端的哪些健康服務器會被選中。 其中經常使用的算法包括:
若是你的應用須要處理狀態而要求用戶能鏈接到和以前相同的服務器。能夠經過 Source 算法基於客戶端的 IP 信息建立關聯,或者使用粘性會話(sticky sessions)。
除此以外,想要解決負載均衡器的單點故障問題,能夠將第二個負載均衡器鏈接到第一個上,從而造成一個集羣。
LVS是 Linux Virtual Server 的簡稱,也就是Linux虛擬服務器。這是一個由章文嵩博士發起的一個開源項目,它的官方網站是LinuxVirtualServer如今 LVS 已是 Linux 內核標準的一部分。使用 LVS 能夠達到的技術目標是:經過 LVS 達到的負載均衡技術和 Linux 操做系統實現一個高性能高可用的 Linux 服務器集羣,它具備良好的可靠性、可擴展性和可操做性。 從而以低廉的成本實現最優的性能。LVS 是一個實現負載均衡集羣的開源軟件項目,LVS架構從邏輯上可分爲調度層、Server集羣層和共享存儲。
LVS的基本工做原理:
LVS的組成:
LVS 由2部分程序組成,包括 ipvs
和 ipvsadm
。
詳細的LVS的介紹能夠參考LVS詳解.
一般傳統的項目體積龐大,需求、設計、開發、測試、部署流程固定。新功能須要在原項目上作修改。
可是微服務能夠看作是對大項目的拆分,是在快速迭代更新上線的需求下產生的。新的功能模塊會發布成新的服務組件,與其餘已發佈的服務組件一同協做。 服務內部有多個生產者和消費者,一般以http rest的方式調用,服務整體以一個(或幾個)服務的形式呈現給客戶使用。
微服務架構是一種思想對微服務架構咱們沒有一個明確的定義,但簡單來講微服務架構是:
採用一組服務的方式來構建一個應用,服務獨立部署在不一樣的進程中,不一樣服務經過一些輕量級交互機制來通訊,例如 RPC、HTTP 等,服務可獨立擴展伸縮,每一個服務定義了明確的邊界,不一樣的服務甚至能夠採用不一樣的編程語言來實現,由獨立的團隊來維護。
Golang的微服務框架kit中有詳細的微服務的例子,能夠參考學習.
微服務架構設計包括:
微服務架構介紹詳細的能夠參考:
在分析分佈式鎖的三種實現方式以前,先了解一下分佈式鎖應該具有哪些條件:
分佈式的CAP理論告訴咱們「任何一個分佈式系統都沒法同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時知足兩項。」因此,不少系統在設計之初就要對這三者作出取捨。在互聯網領域的絕大多數的場景中,都須要犧牲強一致性來換取系統的高可用性,系統每每只須要保證「最終一致性」,只要這個最終時間是在用戶能夠接受的範圍內便可。
一般分佈式鎖以單獨的服務方式實現,目前比較經常使用的分佈式鎖實現有三種:
儘管有這三種方案,可是不一樣的業務也要根據本身的狀況進行選型,他們之間沒有最好只有更適合!
基於數據庫的實現方式的核心思想是:在數據庫中建立一個表,表中包含方法名等字段,並在方法名字段上建立惟一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖。
建立一個表:
DROP TABLE IF EXISTS `method_lock`; CREATE TABLE `method_lock` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名', `desc` varchar(255) NOT NULL COMMENT '備註信息', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
想要執行某個方法,就使用這個方法名向表中插入數據:
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');
由於咱們對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功,那麼咱們就能夠認爲操做成功的那個線程得到了該方法的鎖,能夠執行方法體內容。
成功插入則獲取鎖,執行完成後刪除對應的行數據釋放鎖:
delete from method_lock where method_name ='methodName';
注意:這裏只是使用基於數據庫的一種方法,使用數據庫實現分佈式鎖還有不少其餘的用法能夠實現!
使用基於數據庫的這種實現方式很簡單,可是對於分佈式鎖應該具有的條件來講,它有一些問題須要解決及優化:
一、由於是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,因此,數據庫須要雙機部署、數據同步、主備切換;
二、不具有可重入的特性,由於同一個線程在釋放鎖以前,行數據一直存在,沒法再次成功插入數據,因此,須要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;
三、沒有鎖失效機制,由於有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,因此,須要在表中新增一列,用於記錄失效時間,而且須要有定時任務清除這些失效的數據;
四、不具有阻塞鎖特性,獲取不到鎖直接返回失敗,因此須要優化獲取邏輯,循環屢次去獲取。
五、在實施的過程當中會遇到各類不一樣的問題,爲了解決這些問題,實現方式將會愈來愈複雜;依賴數據庫須要必定的資源開銷,性能問題須要考慮。
選用Redis實現分佈式鎖緣由:
主要實現方式:
使用redis單機來作分佈式鎖服務,可能會出現單點問題,致使服務可用性差,所以在服務穩定性要求高的場合,官方建議使用redis集羣(例如5臺,成功請求鎖超過3臺就認爲獲取鎖),來實現redis分佈式鎖。詳見RedLock。
優勢:性能高,redis可持久化,也能保證數據不易丟失,redis集羣方式提升穩定性。
缺點:使用redis主從切換時可能丟失部分數據。
ZooKeeper是一個爲分佈式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個惟一文件名。基於ZooKeeper實現分佈式鎖的步驟以下:
這裏推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
優勢:具有高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:由於須要頻繁的建立和刪除節點,性能上不如Redis方式。
上面的三種實現方式,沒有在全部場合都是完美的,因此,應根據不一樣的應用場景選擇最適合的實現方式。
在分佈式環境中,對資源進行上鎖有時候是很重要的,好比搶購某一資源,這時候使用分佈式鎖就能夠很好地控制資源。
首先思考下Etcd是什麼?可能不少人第一反應多是一個鍵值存儲倉庫,卻沒有重視官方定義的後半句,用於配置共享和服務發現。
A highly-available key value store for shared configuration and service discovery.
實際上,etcd 做爲一個受到 ZooKeeper 與 doozer 啓發而催生的項目,除了擁有與之相似的功能外,更專一於如下四點。
可是這裏咱們主要講述Etcd如何實現分佈式鎖?
由於 Etcd 使用 Raft 算法保持了數據的強一致性,某次操做存儲到集羣中的值必然是全局一致的,因此很容易實現分佈式鎖。鎖服務有兩種使用方式,一是保持獨佔,二是控制時序。
保持獨佔即全部獲取鎖的用戶最終只有一個能夠獲得。etcd 爲此提供了一套實現分佈式鎖原子操做 CAS(CompareAndSwap)的 API。經過設置prevExist值,能夠保證在多個節點同時去建立某個目錄時,只有一個成功。而建立成功的用戶就能夠認爲是得到了鎖。
控制時序,即全部想要得到鎖的用戶都會被安排執行,可是得到鎖的順序也是全局惟一的,同時決定了執行順序。etcd 爲此也提供了一套 API(自動建立有序鍵),對一個目錄建值時指定爲POST動做,這樣 etcd 會自動在目錄下生成一個當前最大的值爲鍵,存儲這個新的值(客戶端編號)。同時還能夠使用 API 按順序列出全部當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中存儲的值能夠是表明客戶端的編號。
在這裏Ectd實現分佈式鎖基本實現原理爲:
應用示例:
package etcdsync
import ( "fmt" "io" "os" "sync" "time" "github.com/coreos/etcd/client" "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" ) const ( defaultTTL = 60 defaultTry = 3 deleteAction = "delete" expireAction = "expire" ) // A Mutex is a mutual exclusion lock which is distributed across a cluster. type Mutex struct { key string id string // The identity of the caller client client.Client kapi client.KeysAPI ctx context.Context ttl time.Duration mutex *sync.Mutex logger io.Writer } // New creates a Mutex with the given key which must be the same // across the cluster nodes. // machines are the ectd cluster addresses func New(key string, ttl int, machines []string) *Mutex { cfg := client.Config{ Endpoints: machines, Transport: client.DefaultTransport, HeaderTimeoutPerRequest: time.Second, } c, err := client.New(cfg) if err != nil { return nil } hostname, err := os.Hostname() if err != nil { return nil } if len(key) == 0 || len(machines) == 0 { return nil } if key[0] != '/' { key = "/" + key } if ttl < 1 { ttl = defaultTTL } return &Mutex{ key: key, id: fmt.Sprintf("%v-%v-%v", hostname, os.Getpid(), time.Now().Format("20060102-15:04:05.999999999")), client: c, kapi: client.NewKeysAPI(c), ctx: context.TODO(), ttl: time.Second * time.Duration(ttl), mutex: new(sync.Mutex), } } // Lock locks m. // If the lock is already in use, the calling goroutine // blocks until the mutex is available. func (m *Mutex) Lock() (err error) { m.mutex.Lock() for try := 1; try <= defaultTry; try++ { if m.lock() == nil { return nil } m.debug("Lock node %v ERROR %v", m.key, err) if try < defaultTry { m.debug("Try to lock node %v again", m.key, err) } } return err } func (m *Mutex) lock() (err error) { m.debug("Trying to create a node : key=%v", m.key) setOptions := &client.SetOptions{ PrevExist:client.PrevNoExist, TTL: m.ttl, } resp, err := m.kapi.Set(m.ctx, m.key, m.id, setOptions) if err == nil { m.debug("Create node %v OK [%q]", m.key, resp) return nil } m.debug("Create node %v failed [%v]", m.key, err) e, ok := err.(client.Error) if !ok { return err } if e.Code != client.ErrorCodeNodeExist { return err } // Get the already node's value. resp, err = m.kapi.Get(m.ctx, m.key, nil) if err != nil { return err } m.debug("Get node %v OK", m.key) watcherOptions := &client.WatcherOptions{ AfterIndex : resp.Index, Recursive:false, } watcher := m.kapi.Watcher(m.key, watcherOptions) for { m.debug("Watching %v ...", m.key) resp, err = watcher.Next(m.ctx)