Java中的鎖有不少種,常常會聽到「鎖」這個詞。面試
猶如天天出門時,🔑就是一種「鎖」,拿不到🔑,就進不去了。算法
Java那麼多種類的鎖,都是按不一樣標準來分類的。就像商店裏的各類商品,能夠按式樣,也能夠按顏色或者尺寸。編程
其實它們都是一種思想。安全
一個進程能夠包含多個線程,那麼多個線程就會有競爭資源的問題出現,爲了互相不打架,就有了鎖的概念了。 一個線程也能夠本身完成任務,但就像一個小組能夠互相配合、共同完成任務,比一我的要快不少是否是?bash
整理個大圖~多線程
其實這麼多分類只是從特性、表現、實現方式等不一樣的側重點來講的,不是絕對的分類,例如,不可重入鎖和自旋鎖,實際上是同一種鎖。併發
要繞暈了是否是?下面就分別來講說。函數
林妹妹比較悲觀,寶玉比較樂觀~高併發
看名字便知,它是悲觀的,老是想到最壞的狀況。 鎖也會悲觀,它並非難過,它只是很謹慎,怕作錯。性能
每次要讀data的時候,老是以爲其餘人會修改數據,因此先加個🔐,讓其餘人不能改數據,再慢慢讀~
要是你在寫一篇日記,怕別人會偷看了,就加了個打開密碼,別人必須拿到密碼才能打開這篇文章。這就是悲觀鎖了。
應用: synchronized關鍵字和Lock的實現類都是悲觀鎖。
它很樂觀,老是想着最好的狀況。 它比較大條,不會太擔憂。若是要發生,總會發生,若是不會發生,那就不會。爲何要擔憂那麼多?
每次讀data時,老是樂觀地想沒有其餘人會同時修改數據,不用加鎖,放心地讀data。 但在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。
就像和別人共同編輯一篇文章,你在編輯的時候別人也能夠編輯,並且你以爲別人不會改動到你寫的部分,那就是樂觀鎖了。
事事無絕對,悲觀也好樂觀也好,沒有絕對的悲觀,也沒有絕對的樂觀。只是在這個當時,相信,仍是不相信。
類型 | 實現 | 使用場景 | 缺點 |
---|---|---|---|
悲觀鎖 | synchronized關鍵字和Lock的實現類 | 適合寫操做多的場景,能夠保證寫操做時數據正確 | 若是該事務執行時間很長,影響系統的吞吐量 |
樂觀鎖 | 無鎖編程,CAS算法 | 適合讀操做多的場景,可以大幅提高其讀操做的性能 | 若是有外來事務插入,那麼就可能發生錯誤 |
是樂觀鎖的一種實現方式。
簡單來講,有3個三個操做數:
沒有絕對的公平,也沒有絕對的不公平。
公平,就是按順序排隊嘛。 公平鎖維護了一個隊列。要獲取鎖的線程來了都排隊。
非公平,上來就想搶到鎖,好像一個不講道理的,搶不到的話,只好再去乖乖排隊了。 非公平鎖沒有維護隊列的開銷,沒有上下文切換的開銷,可能致使不公平,可是性能比fair的好不少。看這個性能是對誰有利了。
舉個栗子
廣義上的可重入鎖,而不是單指JAVA下的ReentrantLock。
可重入鎖,也叫作遞歸鎖,指的是同一線程外層函數得到鎖以後,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。
這句話神馬意思?
這種鎖是能夠反覆進入的。
當一個線程執行到某個synchronized方法時,好比說method1,而在method1中會調用另一個synchronized方法method2,此時線程沒必要從新去申請鎖,而是能夠直接執行方法method2。
class MyClass {
public synchronized void method1() {
enterNextRoom();
}
public synchronized void method2() {
// todo
}
}
複製代碼
兩個方法method1和method2都用synchronized修飾了。
假設某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而因爲method2也是synchronized方法,假如synchronized不具有可重入性,此時線程A須要從新申請鎖。可是這就會形成一個問題,由於線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
若是不是可重入鎖的話,method2可能不會被當前線程執行,可能形成死鎖。
可重入鎖最大的做用是避免死鎖。
實現類:
按上面的例子,線程A從method1執行到method2的時候,不能直接獲取到鎖,要執行下去,必須先解鎖。
實現不可重入鎖有什麼方式呢?那就是自旋~
(什麼是自旋鎖?等下詳細說,先有個概念,就是當一個線程在獲取鎖的時候,若是鎖已經被其它線程獲取,那麼該線程將循環等待)
public class UnreentrantLock {
private AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
//這句是很經典的「自旋」語法,AtomicInteger中也有
for (;;) {
if (!owner.compareAndSet(null, current)) {
return;
}
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
複製代碼
同一線程兩次調用lock()方法,若是不執行unlock()釋放鎖的話,第二次調用自旋的時候就會產生死鎖。
自旋鎖就是,若是此時拿不到鎖,它不立刻進入阻塞狀態,而願意等待一段時間。
相似於線程在那裏作空循環,若是循環必定的次數還拿不到鎖,那麼它纔會進入阻塞的狀態,這個循環次數是能夠人爲指定的。
有一天去全家買咖啡,服務員說真不巧,前面咖啡機壞了,如今正在修,要等10分鐘喔,剛好沒什麼急事,那就等吧,坐到一邊休息區等10分鐘(其它什麼事都沒作)。介就是自旋鎖~
那是否是有點浪費?若是你等了15分鐘,還沒修好,那你可能不肯意繼續等下去了(15分鐘就是設定的自旋等待的最大時間)
上面說自旋鎖循環的次數是人爲指定的,而自適應旋轉鎖,厲害了,它不須要人爲指定循環次數,它本身自己會判斷要循環幾回,並且每一個線程可能循環的次數也是不同的。
若是這個線程以前拿到過鎖,或者常常拿到一個鎖,那它本身判斷下來再次拿到這個鎖的機率很大,循環次數就大一些;若是這個線程以前沒拿到過這個鎖,那它就沒把握了,怕消耗CPU,循環次數就小一點。
它解決的是「鎖競爭時間不肯定」的問題,但也不必定它本身設定的必定合適。
仍是前面去全家等咖啡的栗子吧~ 要是等到5分鐘,還沒修好,你目測10分鐘裏也修很差,就再也不等下去了(循環次數小);要是等了10分鐘了,服務員說很是抱歉,快了快了,再1分鐘就能夠用了,你也還不急,都已經等了10分鐘了,就多等一下子嘛(循環次數大)
簡單實現以下:
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
複製代碼
lock()方法利用CAS,當第一個線程A獲取鎖的時候,可以成功獲取到,不會進入while循環;
若是此時線程A沒有釋放鎖,另外一個線程B又來獲取鎖,此時因爲不知足CAS,因此就會進入while循環,不斷判斷是否知足CAS,直到A線程調用unlock方法釋放了該鎖,線程B才能獲取鎖。
自旋鎖是不阻塞鎖,可是它也會等一段時間,那和阻塞鎖有什麼區別?
去一個熱門飯店吃飯,到了門口一看,門口的座位坐滿了人……這咋整……服務員說,您能夠先拿個號~小票上掃個二維碼,關注我們,輪到您了,服務號裏就會有提示噠~(很熟悉是否是?) 而後你就先取了號去逛逛周圍小店去了,等輪到你了,手機裏收到一條服務提醒消息,到你啦~這時你再去,就能夠進店了。
這就是阻塞的過程~
若是是自旋鎖呢? 就是你本身其它事情都不作,等在那裏,就像去超市排隊結帳同樣,你走開的話是沒有人會通知你的,只能從新排隊,須要本身時刻檢查有沒有排到(能不能訪問到共享資源)。
插播一下:
阻塞或喚醒一個Java線程須要操做系統切換CPU狀態來完成,這種狀態轉換須要耗費處理器時間。
複製代碼
自旋鎖 | 阻塞鎖 | |
---|---|---|
改變線程狀態? | 不改變線程運行狀態,一直處於用戶態,即線程一直都是active的 | 改變線程運行狀態,讓線程進入阻塞狀態進行等待 |
佔用CPU? | 佔用CPU時間 | 不會佔用CPU時間,不會致使 CPU佔用率太高,但進入時間以及恢復時間都要比自旋鎖略慢 |
適用場景 | 線程競爭不激烈,而且保持鎖的時間段 | 競爭激烈的狀況下 阻塞鎖的性能要明顯高於自旋鎖 |
這四種狀態都不是Java語言中的鎖,而是Jvm爲了提升鎖的獲取與釋放效率而作的優化(使用synchronized時),它們會隨着競爭的激烈而逐漸升級,而且是不可逆的升級。
P.S., 無鎖,即沒有鎖~
若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性,所以會有一些代碼天生就是線程安全的。
CAS算法 即compare and swap(比較與交換),就是有名的無鎖算法。
狀態 | 描述 | 優勢 | 缺點 | 應用場景 | 適用場景 |
---|---|---|---|---|---|
偏向鎖 | 無實際競爭,讓一個線程一直持有鎖,在其餘線程須要競爭鎖的時候,再釋放鎖 | 加鎖解鎖不須要額外消耗 | 若是線程間存在競爭,會有撤銷鎖的消耗 | 只有一個線程進入臨界區 | 適用於只有一個線程訪問同步塊場景。 |
輕量級 | 無實際競爭,多個線程交替使用鎖;容許短期的鎖競爭 | 競爭的線程不會阻塞 | 若是線程一直得不到鎖,會一直自旋,消耗CPU | 多個線程交替進入臨界區 | 追求響應時間。同步塊執行速度很是快。 |
重量級 | 有實際競爭,且鎖競爭時間長 | 線程競爭不使用自旋,不消耗CPU | 線程阻塞,響應時間長 | 多個線程同時進入臨界區 | 追求吞吐量。同步塊執行速度較長。 |
你常常去一家店坐在同一個位置吃飯,老闆已經記住你啦,每次你去的時候,只要店裏客人很少,老闆都會給你留着那個座位,這個座位就是你的「偏向鎖」,每次只有你這一個線程用。
有一天你去的時候,店裏已經坐滿了,你的位置也被別人坐了,你只能等着(進入競爭狀態),這時那個座位就升級到「輕量級」了。
要是那個座位特別好(臨窗風景最佳,能隔江賞月~)每次你到的時候,都有其餘好幾我的也要去搶那個位置,沒坐到那個位置就不吃飯了>_< 那時那個座位就升級到「重量級」了。
輕量級鎖和重量級鎖的重要區別是: 拿不到「鎖」時,是否有線程調度和上下文切換的開銷。
簡單來講:若是發現同步週期內都是不存在競爭,JVM會使用CAS操做來替代操做系統互斥量。這個優化就被叫作輕量級鎖。
相比重量級鎖,其加鎖和解鎖的開銷會小不少。重量級鎖的「重」,關鍵在於線程上下文切換的開銷大。
共享 Vs 獨享 圖~ 是否是很形象? 😄
類型 | 描述 | 實現類 |
---|---|---|
共享(讀鎖) | 可被多個線程所持有,其餘用戶能夠併發讀取數據。若是事務T對數據A加上共享鎖後,則其餘事務只能對A再加共享鎖,不能加排他鎖。獲准共享鎖的事務只能讀數據,不能修改數據。 | ReentrantReadWriteLock裏的讀鎖 |
獨享(排他鎖,寫鎖) | 一次只能被一個線程所持有。若是事務T對數據A加上排它鎖後,則其餘事務不能再對A加任何類型的鎖。得到排它鎖的事務即能讀數據又能修改數據。 | synchronized、ReentrantLock、ReentrantReadWriteLock裏的寫鎖 |
排它鎖是悲觀鎖的一種實現。
問題來了:共享鎖爲何要加個「讀鎖」?
防止數據在被讀取的時候被別的線程加上寫鎖。
複製代碼
而獨佔鎖的原理是:
若是有線程獲取到鎖,那麼其它線程只能是獲取鎖失敗,而後進入等待隊列中等待被喚醒。
複製代碼
小組每一個禮拜都要各個成員共同填一份週報表格,要是每一個人打開的時候,能夠加一個寫鎖,即你在寫的時候,別人不能修改,這就是獨享鎖(寫鎖); 可是這份表格你們能夠同時打開,看到表格內容(讀取數據),正在改數據的人能夠對這份表格加上共享鎖,那這個鎖就是共享鎖。
共享鎖的獲取方法爲acquireShared,源碼爲:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
複製代碼
當返回值爲大於等於0的時候方法結束說明得到成功獲取鎖,不然,代表獲取同步狀態失敗即所引用的線程獲取鎖失敗,會執行doAcquireShared方法.
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製代碼
簡單來講 addWaiter(Node mode) 方法作了如下事情:
建立基於當前線程的獨佔式類型的節點; 利用 CAS 原子操做,將節點加入隊尾。
ReentrantLock 是一個獨佔/排他鎖。
提供了是否公平鎖的初始化:
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製代碼
使用ReentrantLock必須在finally控制塊中進行解鎖操做。
在資源競爭不激烈的情形下,性能稍微比synchronized差點點。可是當同步很是激烈的時候,synchronized的性能一會兒能降低好幾十倍,而ReentrantLock確還能維持常態。
高併發量狀況下使用ReentrantLock。
優勢: 可必定程度避免死鎖。
對Java的各類鎖概念作了下整理,寫了些本身的理解, 還有不少基礎方面,好比Java的對象頭、對象模型(都比較基礎)、鎖的優化、各種鎖代碼實現等,後續再補充下。 有不少公號有不少高水平的文章,須要理解和練習的有太多。