來自:苦逼的碼農(微訊號:di201805)
作者:帥地
個人簡介:一個熱愛程式設計的在校生,我的世界不只有coding,還有writing。目前維護訂閱號「苦逼的碼農」,專註於寫「演演算法與資料結構」,「Java」,「計算機網路」。
重量級鎖?自旋鎖?自適應自旋鎖?輕量級鎖?偏向鎖?悲觀鎖?樂觀鎖?執行一個方法咋這麼辛苦,到處都是鎖。
今天這篇文章,給大家普及下這些鎖究竟是啥,他們的由來,他們之間有啥關係,有啥區別。
重量級鎖
如果你學過多執行緒,那麼你肯定知道鎖這個東西,至於為什麼需要鎖,我就不給你普及了,就當做你是已經懂的了。
我們知道,我們要進入一個同步、執行緒安全的方法時,是需要先獲得這個方法的鎖的,退出這個方法時,則會釋放鎖。如果獲取不到這個鎖的話,意味著有別的執行緒在執行這個方法,這時我們就會馬上進入阻塞的狀態,等待那個持有鎖的執行緒釋放鎖,然後再把我們從阻塞的狀態喚醒,我們再去獲取這個方法的鎖。
這種獲取不到鎖就馬上進入阻塞狀態的鎖,我們稱之為重量級鎖。
自旋鎖
我們知道,執行緒從執行態進入阻塞態這個過程,是非常耗時的,因為不僅需要儲存執行緒此時的執行狀態,背景關係等資料,還涉及到使用者態到核心態的轉換。當然,把執行緒從阻塞態喚醒也是一樣,也是非常消耗時間的。
剛才我說執行緒拿不到鎖,就會馬上進入阻塞狀態,然而現實是,它雖然這一刻拿不到鎖,可能在下 0.0001 秒,就有其他執行緒把這個鎖釋放了。如果它慢0.0001秒來拿這個鎖的話,可能就可以順利拿到了,不需要經歷阻塞/喚醒這個花時間的過程了。
然而重量級鎖就是這麼坑,它就是不肯等待一下,一拿不到就是要馬上進入阻塞狀態。為瞭解決這個問題,我們引入了另外一種願意等待一段時間的鎖 — 自旋鎖。
自旋鎖就是,如果此時拿不到鎖,它不馬上進入阻塞狀態,而是等待一段時間,看看這段時間有沒其他人把這鎖給釋放了。怎麼等呢?這個就類似於執行緒在那裡做空迴圈,如果迴圈一定的次數還拿不到鎖,那麼它才會進入阻塞的狀態。
至於是迴圈等待幾次,這個是可以人為指定一個數字的。
自適應自旋鎖
上面我們說的自旋鎖,每個執行緒迴圈等待的次數都是一樣的,例如我設定為 100次的話,那麼執行緒在空迴圈 100 次之後還沒拿到鎖,就會進入阻塞狀態了。
而自適應自旋鎖就牛逼了,它不需要我們人為指定迴圈幾次,它自己本身會進行判斷要迴圈幾次,而且每個執行緒可能迴圈的次數也是不一樣的。而之所以這樣做,主要是我們覺得,如果一個執行緒在不久前拿到過這個鎖,或者它之前經常拿到過這個鎖,那麼我們認為它再次拿到鎖的機率非常大,所以迴圈的次數會多一些。
而如果有些執行緒從來就沒有拿到過這個鎖,或者說,平時很少拿到,那麼我們認為,它再次拿到的機率是比較小的,所以我們就讓它迴圈的次數少一些。因為你在那裡做空迴圈是很消耗 CPU 的。
所以這種能夠根據執行緒最近獲得鎖的狀態來調整迴圈次數的自旋鎖,我們稱之為自適應自旋鎖。
輕量級鎖
上面我們介紹的三種鎖:重量級、自旋鎖和自適應自旋鎖,他們都有一個特點,就是進入一個方法的時候,就會加上鎖,退出一個方法的時候,也就釋放對應的鎖。
之所以要加鎖,是因為他們害怕自己在這個方法執行的時候,被別人偷偷進來了,所以只能加鎖,防止其他執行緒進來。這就相當於,每次離開自己的房間,都要鎖上門,人回來了再把鎖解開。
這實在是太麻煩了,如果根本就沒有執行緒來和他們競爭鎖,那他們不是白白上鎖了?要知道,加鎖這個過程是需要作業系統這個大佬來幫忙的,是很消耗時間的,。為瞭解決這種動不動就加鎖帶來的開銷,輕量級鎖出現了。
輕量級鎖認為,當你在方法裡面執行的時候,其實是很少剛好有人也來執行這個方法的,所以,當我們進入一個方法的時候根本就不用加鎖,我們只需要做一個標記就可以了,也就是說,我們可以用一個變數來記錄此時該方法是否有人在執行。也就是說,如果這個方法沒人在執行,當我們進入這個方法的時候,採用CAS機制,把這個方法的狀態標記為已經有人在執行,退出這個方法時,在把這個狀態改為了沒有人在執行了。
之所以要用CAS機制來改變狀態,是因為我們對這個狀態的改變,不是一個原子性操作,所以需要CAS機制來保證操作的原子性。不知道CAS的可以看這篇文章:併發的核心:CAS 是什麼?Java8是如何最佳化 CAS 的?。
顯然,比起加鎖操作,這個採用CAS來改變狀態的操作,花銷就小多了。
然而可能會說,沒人來競爭的這種想法,那是你說的而已,那如果萬一有人來競爭說呢?也就是說,當一個執行緒來執行一個方法的時候,方法裡面已經有人在執行了。
如果真的遇到了競爭,我們就會認為輕量級鎖已經不適合了,我們就會把輕量級鎖升級為重量級鎖了。
所以輕量級鎖適合用在那種,很少出現多個執行緒競爭一個鎖的情況,也就是說,適合那種多個執行緒總是錯開時間來獲取鎖的情況。
偏向鎖
偏向鎖就更加牛逼了,我們已經覺得輕量級鎖已經夠輕,然而偏向鎖更加省事,偏向鎖認為,你輕量級鎖每次進入一個方法都需要用CAS來改變狀態,退出也需要改變,多麻煩。
偏向鎖認為,其實對於一個方法,是很少有兩個執行緒來執行的,搞來搞去,其實也就一個執行緒在執行這個方法而已,相當於單執行緒的情況,居然是單執行緒,那就沒必要加鎖了。
不過畢竟實際情況的多執行緒,單執行緒只是自己認為的而已了,所以呢,偏向鎖進入一個方法的時候是這樣處理的:如果這個方法沒有人進來過,那麼一個執行緒首次進入這個方法的時候,會採用CAS機制,把這個方法標記為有人在執行了,和輕量級鎖加鎖有點類似,並且也會把該執行緒的 ID 也記錄進去,相當於記錄了哪個執行緒在執行。
然後,但這個執行緒退出這個方法的時候,它不會改變這個方法的狀態,而是直接退出來,懶的去改,因為它認為除了自己這個執行緒之外,其他執行緒並不會來執行這個方法。
然後當這個執行緒想要再次進入這個方法的時候,會判斷一下這個方法的狀態,如果這個方法已經被標記為有人在執行了,並且執行緒的ID是自己,那麼它就直接進入這個方法執行,啥也不用做
你看,多方便,第一次進入需要CAS機制來設定,以後進出就啥也不用幹了,直接進入退出。
然而,現實總是殘酷的,畢竟實際情況還是多執行緒,所以萬一有其他執行緒來進入這個方法呢?如果真的出現這種情況,其他執行緒一看這個方法的ID不是自己,這個時候說明,至少有兩個執行緒要來執行這個方法論,這意味著偏向鎖已經不適用了,這個時候就會從偏向鎖升級為輕量級鎖。
所以呢,偏向鎖適用於那種,始終只有一個執行緒在執行一個方法的情況哦。
這裡我作下說明,為了方便大家理解,我在將輕量級鎖和偏向鎖的時候,其實是簡化了很多的,不然的話會涉及到物件的內部結構、佈局,我覺得把那些扯出來,你們可能要暈了,所以我大致講了他們的原理。
悲觀鎖和樂觀鎖
最開始我們說的三種鎖,重量級鎖、自旋鎖和自適應自旋鎖,進入方法之前,就一定要先加一個鎖,這種我們為稱之為悲觀鎖。悲觀鎖總認為,如果不事先加鎖的話,就會出事,這種想法確實悲觀了點,這估計就是悲觀鎖的來源了。
而樂觀鎖卻相反,認為不加鎖也沒事,我們可以先不加鎖,如果出現了衝突,我們在想辦法解決,例如 CAS 機制,上面說的輕量級鎖,就是樂觀鎖的。不會馬上加鎖,而是等待真的出現了衝突,在想辦法解決。不知道 CAS 機制的,可以看我之前寫的這篇文章哦:併發的核心:CAS 是什麼?Java8是如何最佳化 CAS 的?。
總結
到這裡也大致寫完了,簡單介紹普及了一下,重點的大家要理解他們的由來,原理。每一種鎖都有他們的應用以及各自的優缺點,如果有機會,我再給大家說說他們各自的應用場景,優缺點,這個面試的時候,好像也會被經常到,今天先寫到這裡勒。
大家可以說說這些鎖的優缺點哦,例如與重量級鎖相比,自旋鎖容量導致什麼問題的發生?悲觀鎖和樂觀鎖的比較呢?大家也可以評論區說說勒,這些是一定要搞懂的哦。
朋友會在“發現-看一看”看到你“在看”的內容