多執行緒方向的鎖
- 2019 年 10 月 4 日
- 筆記
版權聲明:本文為部落客原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接:https://blog.csdn.net/qq_37933685/article/details/80767809
個人部落格:https://suveng.github.io/blog/
多執行緒方向的鎖
注意:
環境說明:
- IDE: IntelliJ IDEA 2017
- java version: jdk1.8
- GitHub程式碼地址:https://github.com/1344115844/learning
簡單鎖
簡單的給資源加把鎖;以下的所有程式碼實現都使用同一個類文件.
Counter.java
public class Counter { private OrdinaryLock lock = new OrdinaryLock(); private int count = 0; public int inc() throws InterruptedException { Thread.sleep(2000); lock.lock();//注釋掉看區別:TODO System.out.println(Thread.currentThread().getId() + "前..." + this.count); this.count++; System.out.println(Thread.currentThread().getId() + "後..." + this.count); lock.unlock();//注釋掉看區別:TODO return count; } public static void main(String[] args) throws InterruptedException { final Counter counter = new Counter(); final ReentrantClazz reentrantClazz = new ReentrantClazz(); final ReentrantLock reentrantLock =new ReentrantLock(); for (int i = 0; i < 10; i++) { new Thread(new Runnable() { public void run() { try { counter.inc();//OrdinaryLock實例 //reentrantClazz.outer();//重入鎖實例 這是重入鎖的我現在先注釋掉 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } class OrdinaryLock { private boolean isLocked = false; public synchronized void lock() throws InterruptedException { while (isLocked) { //不用if,而用while,是為了防止假喚醒 wait(); } isLocked = true; } public synchronized void unlock() { isLocked = false; notify(); } }
如上程式碼,我先把inc()的lock注釋掉;運行結果如下:
控制台列印無序,而且前後相差不止1;具體邏輯看inc()方法.
12前...0 16前...0 20前...1 17前...0 17後...4 18前...0 15前...0 14前...0 13前...0 11前...0 13後...8 14後...7 15後...6 18後...5 20後...3 16後...2 19前...1 19後...10 12後...1 11後...9 Process finished with exit code 0
當我加了普通鎖之後,把注釋放開,就會有序,而且保證執行緒加減前後相差是1.運行結果如下所示
11前...0 11後...1 13前...1 13後...2 14前...2 14後...3 12前...3 12後...4 15前...4 15後...5 16前...5 16後...6 17前...6 17後...7 19前...7 19後...8 18前...8 18後...9 20前...9 20後...10 Process finished with exit code 0
這就是加鎖的魅力,但是同時也會損失效率和響應速度.
重入鎖
重進入是指任意執行緒在獲取到鎖之後,再次獲取該鎖而不會被該鎖所阻塞。關聯一個執行緒持有者+計數器,重入意味著鎖操作的顆粒度為「執行緒」。 重入鎖的實現方式:每個鎖關聯一個執行緒持有者和計數器,當計數器為0時表示該鎖沒有被任何執行緒持有,那麼任何執行緒都可能獲得該鎖而調用相應的方法;當某一執行緒請求成功後,JVM會記下鎖的持有執行緒,並且將計數器置為1;此時其它執行緒請求該鎖,則必須等待;而該持有鎖的執行緒如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當執行緒退出同步程式碼塊時,計數器會遞減,如果計數器為0,則釋放該鎖
接下來先演示沒有重入特性的普通鎖,當內部方法想要獲取鎖的時候就會陷入死鎖.
程式碼如下,在Counter.java裡面新增內部類,然後在main()方法中調用這個類的outer方法.outer()調用inner(),inner()也嘗試獲取鎖.結果會陷入死鎖
synchronized:可重入鎖; java.util.concurrent.locks.ReentrantLock:可重入鎖;
//重入示例,outer調用inner 方法 //lock可以使用origin lock 或者reentrant lock //使用origin lock 會造成死鎖 reentrant lock 不會 class ReentrantClazz { OrdinaryLock lock = new OrdinaryLock();//:TODO 修改為reentrantLock 再運行即可 public void outer() throws InterruptedException { lock.lock(); System.out.println("進入outter"); inner(); lock.unlock(); } public void inner() throws InterruptedException { System.out.println("進入inner"); lock.lock(); //do something lock.unlock(); } }
運行結果如下,
沒有結束,只是獲取並沒有釋放,因為上面使用的lock是OrdinaryLock,接下來把這個換成可重入鎖ReentrantLock,
ReentrantClazz類中的OrdinaryLock lock = new OrdinaryLock();
,換成ReentrantLock lock = new ReentrantLock();
//可重入的鎖 class ReentrantLock { private boolean isLocked = false; private Thread lockedBy = null; private int lockedCount = 0; public synchronized void lock() throws InterruptedException { Thread callingThread = Thread.currentThread(); while (isLocked && lockedBy != callingThread) { wait(); } isLocked = true; lockedCount++; lockedBy = callingThread; } public synchronized void unlock() { if (Thread.currentThread() == this.lockedBy) { lockedCount--; if (lockedCount == 0) { isLocked = false; notify(); } } } }
運行結果如下:
11進入outter 11執行緒outer()方法獲取了鎖 11進入inner 11執行緒inner()方法獲取了鎖 11執行緒inner()方法釋放了鎖 11執行緒outer()方法釋放了鎖 12進入outter 12執行緒outer()方法獲取了鎖 12進入inner 12執行緒inner()方法獲取了鎖 12執行緒inner()方法釋放了鎖 12執行緒outer()方法釋放了鎖 13進入outter 13執行緒outer()方法獲取了鎖 13進入inner 13執行緒inner()方法獲取了鎖 13執行緒inner()方法釋放了鎖 13執行緒outer()方法釋放了鎖 14進入outter 14執行緒outer()方法獲取了鎖 14進入inner 14執行緒inner()方法獲取了鎖 14執行緒inner()方法釋放了鎖 14執行緒outer()方法釋放了鎖 15進入outter 15執行緒outer()方法獲取了鎖 15進入inner 15執行緒inner()方法獲取了鎖 15執行緒inner()方法釋放了鎖 15執行緒outer()方法釋放了鎖 16進入outter 16執行緒outer()方法獲取了鎖 16進入inner 16執行緒inner()方法獲取了鎖 16執行緒inner()方法釋放了鎖 16執行緒outer()方法釋放了鎖 17進入outter 17執行緒outer()方法獲取了鎖 17進入inner 17執行緒inner()方法獲取了鎖 17執行緒inner()方法釋放了鎖 17執行緒outer()方法釋放了鎖 18進入outter 18執行緒outer()方法獲取了鎖 18進入inner 18執行緒inner()方法獲取了鎖 18執行緒inner()方法釋放了鎖 18執行緒outer()方法釋放了鎖 19進入outter 19執行緒outer()方法獲取了鎖 19進入inner 19執行緒inner()方法獲取了鎖 19執行緒inner()方法釋放了鎖 19執行緒outer()方法釋放了鎖 20進入outter 20執行緒outer()方法獲取了鎖 20進入inner 20執行緒inner()方法獲取了鎖 20執行緒inner()方法釋放了鎖 20執行緒outer()方法釋放了鎖 Process finished with exit code 0
很明顯每個執行緒的outer和inner都能獲取並釋放鎖.這就是可重入鎖.
GitHub程式碼地址:https://github.com/1344115844/learning
自旋鎖
自旋鎖的核心:不放棄時間片。執行緒獲取不到鎖,就會被阻塞掛起,等其他執行緒釋放鎖的時候,才被喚醒起來。執行緒掛起和喚醒是需要轉入到內核態完成的,這些操作對系統的並發性能會帶來影響。其實有時候執行緒雖然沒法立刻獲取到鎖,但是也可能很快就會獲取到鎖。JVM採用了一種叫自旋鎖的機制,讓獲取不到鎖的執行緒執行一個空的循環,一段時間後,如果還是沒法獲取鎖,執行緒才會被掛起。 如果鎖競爭不嚴重的情況下,且任務執行時間不長,那麼可以嘗試使用自旋鎖。
自旋鎖可能引起的問題:
- 過多佔據CPU時間:如果鎖的當前持有者長時間不釋放該鎖,那麼等待者將長時間的佔據cpu時間片,導致CPU資源的浪費,因此可以設定一個時間,當鎖持有者超過這個時間不釋放鎖時,等待者會放棄CPU時間片阻塞;
- 死鎖問題:試想一下,有一個執行緒連續兩次試圖獲得自旋鎖(比如在遞歸程式中),第一次這個執行緒獲得了該鎖,當第二次試圖加鎖的時候,檢測到鎖已被佔用(其實是被自己佔用),那麼這時,執行緒會一直等待自己釋放該鎖,而不能繼續執行,這樣就引起了死鎖。因此遞歸程式使用自旋鎖應該遵循以下原則:遞歸程式決不能在持有自旋鎖時調用它自己,也決不能在遞歸調用時試圖獲得相同的自旋鎖。
程式碼實現自旋鎖的不可重入鎖:
BadSpinLock.java
/** * @author Veng Su [email protected] * @date 2018/6/18 8:49 */ import java.util.concurrent.atomic.AtomicReference; /** *@author Veng Su 2018/6/18 8:53 *不可重入的自旋鎖 **/ public class BadSpinLock { AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的執行緒對象 public void lock() { Thread cur = Thread.currentThread(); while (!owner.compareAndSet(null, cur)) { System.out.println(cur.getId()+ " 自旋中"); } System.out.println(cur.getId()+"執行緒上鎖成功"); } public void unLock() { Thread cur = Thread.currentThread(); if (cur == owner.get()) { owner.compareAndSet(cur, null); System.out.println(cur.getId()+ " 釋放了鎖"); } } }
這裡是沒有使用count進行執行緒的獲取鎖的計數.會陷入死鎖
main方法如下:
public static void main(String[] args) throws InterruptedException { final Counter counter = new Counter(); final ReentrantClazz reentrantClazz = new ReentrantClazz(); final ReentrantLock reentrantLock =new ReentrantLock(); final SpinLock spinLock =new SpinLock(); for (int i = 0; i < 1; i++) { new Thread(new Runnable() { public void run() { try { // counter.inc();//OrdinaryLock實例 reentrantClazz.outer();//重入鎖實例 } catch (Exception e) { e.printStackTrace(); } } }).start(); } } //重入示例,outer調用inner 方法 //lock可以使用origin lock 或者reentrant lock //使用origin lock 會造成死鎖 reentrant lock 不會 class ReentrantClazz { BadSpinLock lock = new BadSpinLock();//:TODO 修改為BadSpinLock 再運行即可 public void outer() throws InterruptedException { System.out.println(Thread.currentThread().getId()+"進入outter"); lock.lock(); System.out.println(Thread.currentThread().getId()+"執行緒outer()方法獲取了鎖"); inner(); lock.unLock(); System.out.println(Thread.currentThread().getId()+"執行緒outer()方法釋放了鎖"); } public void inner() throws InterruptedException { System.out.println(Thread.currentThread().getId()+"進入inner"); lock.lock(); System.out.println(Thread.currentThread().getId()+"執行緒inner()方法獲取了鎖"); //do something lock.unLock(); System.out.println(Thread.currentThread().getId()+"執行緒inner()方法釋放了鎖"); } }
運行結果如下:
進入inner方法後就獲取不到鎖了,這是不可重入鎖,造成死鎖.
程式碼實現自旋鎖的可重入鎖
SpinLock.java
import java.util.concurrent.atomic.AtomicReference; /** * @author Veng Su [email protected] * @date 2018/6/18 8:35 * 可重入的自旋鎖 */ public class SpinLock { AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的執行緒對象 private int count;//用一個計數器 來做 重入鎖獲取次數的計數 public void lock() { Thread cur = Thread.currentThread(); if (cur == owner.get()) { count++; return; } while (!owner.compareAndSet(null, cur)) {//當執行緒越來越多 由於while循環 會浪費CPU時間片,CompareAndSet 需要多次對同一記憶體進行訪問 //會造成記憶體的競爭,然而對於X86,會採取競爭記憶體匯流排的方式來訪問記憶體,所以會造成記憶體訪問速度下降(其他執行緒老訪問快取),因而會影響整個系統的性能 System.out.println(Thread.currentThread().getId()+"執行緒自旋中...."); } } public void unLock() { Thread cur = Thread.currentThread(); if (cur == owner.get()) { if (count > 0) { count--; } else { owner.compareAndSet(cur, null); } } } }
然後把reentrantClazz類的lock換成spinlock;
運行結果如下:
11進入outter 11執行緒outer()方法獲取了鎖 11進入inner 11執行緒inner()方法獲取了鎖 11執行緒inner()方法釋放了鎖 11執行緒outer()方法釋放了鎖 12進入outter 12執行緒outer()方法獲取了鎖 12進入inner 12執行緒inner()方法獲取了鎖 12執行緒inner()方法釋放了鎖 12執行緒outer()方法釋放了鎖 13進入outter 13執行緒outer()方法獲取了鎖 13進入inner 13執行緒inner()方法獲取了鎖 13執行緒inner()方法釋放了鎖 13執行緒outer()方法釋放了鎖 14進入outter 14執行緒outer()方法獲取了鎖 14進入inner 14執行緒inner()方法獲取了鎖 14執行緒inner()方法釋放了鎖 14執行緒outer()方法釋放了鎖 16進入outter 16執行緒outer()方法獲取了鎖 16進入inner 16執行緒inner()方法獲取了鎖 16執行緒inner()方法釋放了鎖 16執行緒outer()方法釋放了鎖 17進入outter 17執行緒outer()方法獲取了鎖 17進入inner 17執行緒inner()方法獲取了鎖 17執行緒inner()方法釋放了鎖 18進入outter 17執行緒outer()方法釋放了鎖 18執行緒outer()方法獲取了鎖 18進入inner 18執行緒inner()方法獲取了鎖 18執行緒inner()方法釋放了鎖 18執行緒outer()方法釋放了鎖 19進入outter 19執行緒outer()方法獲取了鎖 19進入inner 19執行緒inner()方法獲取了鎖 19執行緒inner()方法釋放了鎖 19執行緒outer()方法釋放了鎖 20進入outter 20執行緒outer()方法獲取了鎖 20進入inner 20執行緒inner()方法獲取了鎖 20執行緒inner()方法釋放了鎖 20執行緒outer()方法釋放了鎖 15進入outter 15執行緒outer()方法獲取了鎖 15進入inner 15執行緒inner()方法獲取了鎖 15執行緒inner()方法釋放了鎖 15執行緒outer()方法釋放了鎖 Process finished with exit code 0
公平鎖和非公平鎖
ReentrantLock鎖的實現分析
ReentrantLock
的公平鎖和非公平鎖都委託了 AbstractQueuedSynchronizer#acquire
去請求獲取。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }12345
tryAcquire
是一個抽象方法,是公平與非公平的實現原理所在。addWaiter
是將當前執行緒結點加入等待隊列之中。公平鎖在鎖釋放後會嚴格按照等到隊列去取後續值,而非公平鎖在對於新晉執行緒有很大優勢。acquireQueued
在多次循環中嘗試獲取到鎖或者將當前執行緒阻塞。selfInterrupt
如果執行緒在阻塞期間發生了中斷,調用Thread.currentThread().interrupt()
中斷當前執行緒。
ReentrantLock
對執行緒的阻塞是基於LockSupport.park(this);
(見AbstractQueuedSynchronizer#parkAndCheckInterrupt
)。 先決條件是當前節點有限次嘗試獲取鎖失敗。
公平鎖和非公平鎖在說的獲取上都使用到了 volatile 關鍵字修飾的state欄位, 這是保證多執行緒環境下鎖的獲取與否的核心。 但是當並發情況下多個執行緒都讀取到 state == 0時,則必須用到CAS技術,一門CPU的原子鎖技術,可通過CPU對共享變數加鎖的形式,實現數據變更的原子操作。 volatile 和 CAS的結合是並發搶佔的關鍵。
公平鎖FairSync
公平鎖的實現機理在於每次有執行緒來搶佔鎖的時候,都會檢查一遍有沒有等待隊列,如果有, 當前執行緒會執行如下步驟:
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; }12345
其中hasQueuedPredecessors
是用於檢查是否有等待隊列的。
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }1234567
非公平鎖NonfairSync
非公平鎖在實現的時候多次強調隨機搶佔:
if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }123456
與公平鎖的區別在於新晉獲取鎖的進程會有多次機會去搶佔鎖。如果被加入了等待隊列後則跟公平鎖沒有區別。
ReentrantLock鎖的釋放
ReentrantLock鎖的釋放是逐級釋放的,也就是說在 可重入性 場景中,必須要等到場景內所有的加鎖的方法都釋放了鎖, 當前執行緒持有的鎖才會被釋放! 釋放的方式很簡單, state欄位減一即可:
protected final boolean tryRelease(int releases) { // releases = 1 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }12345678910111213
ReentrantLock等待隊列中元素的喚醒
噹噹前擁有鎖的執行緒釋放鎖之後, 且非公平鎖無執行緒搶佔,就開始執行緒喚醒的流程。 通過tryRelease
釋放鎖成功,調用LockSupport.unpark(s.thread);
終止執行緒阻塞。 見程式碼:
private void unparkSuccessor(Node node) { // 強行回寫將被喚醒執行緒的狀態 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; // s為h的下一個Node, 一般情況下都是非Null的 if (s == null || s.waitStatus > 0) { s = null; // 否則按照FIFO原則尋找最先入隊列的並且沒有被Cancel的Node for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } // 再喚醒它 if (s != null) LockSupport.unpark(s.thread); }123456789101112131415161718
ReentrantLock記憶體可見性分析
針對如下程式碼:
try { lock.lock(); i ++; } finally { lock.unlock(); }123456
可以發現哪怕在不使用 volatile
關鍵字修飾元素i
的時候, 這裡的i
也是沒有並發問題的。
互斥鎖
保證在同一時刻只有一個執行緒對其進行操作。比如最常見的 synchronized。