java基礎之ReentrantLock鎖


Lock鎖的公平性和非公平性

1、lock鎖項目使用

在項目中的使用方式:

public class AQSTestOne {
    // 使用公平鎖來進行測試
    private static final Lock LOCK = new ReentrantLock(true);

    public static void main(String[] args) {
        LOCK.lock();
        try {
            System.out.println("so something  ");
        }catch (Exception e){
            System.out.println("do Exception something............");
        }finally {
            LOCK.unlock();
        }
    }
}

因為對於對象來說,對於成員變量LOCK鎖來說,會在堆內存中,任何一個線程進來的時候執行了對應的方法,都會執行到lock鎖上來進行排隊。

每個線程都會來使用lock鎖,那么lock.lock()方法是如何保證多線程環境下,在JVM中在某一個時刻,只有一個線程占用鎖呢?

2、AQS繼承體系

對於ReentrantLock類中,存在AbstractQueuedSynchronizer類以及對應的子類Sync和Sync的兩個子類:FairSync和NonfairSync

對應的是公平同步鎖和非公平同步鎖。

3、構造函數

首先從構造函數來講起,因為非公平鎖的效率高,所以推薦使用的是非公平鎖。

從構造函數中可以看到對應的結構:

   private final Sync sync;

   public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
	
	// 默認采用非公平鎖
    public ReentrantLock() {
        sync = new NonfairSync();
    }

	
	// 繼承體系圖
    abstract static class Sync extends AbstractQueuedSynchronizer {

        abstract void lock();
        
    }        

4、加鎖流程

lock鎖是如何保證多線程能夠保證線程安全呢?

那么看一下lock鎖又是如何來進行加鎖的。

首先來看這行代碼到底做了什么?

lock.lock();

進源碼查看:

    public void lock() {
        sync.lock();
    }

那么這里看公平鎖的實現方式:

        final void lock() {
            acquire(1);
        }

重點就來到了acquire方法,看看對應的代碼實現:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

通過代碼可以看到首先會來嘗試獲取。

可以根據代碼來畫一幅流程圖來描述是如何來獲取得到鎖的:

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 判斷當前鎖是否被其他線程持有!利用一個int類型的變量來進行修飾
            int c = getState();
            if (c == 0) {
                // 這里判斷是無鎖狀態之后,並不是直接獲取得到鎖,還需要來進行判斷CLH隊列中是否有線程在排隊
                // 因為這里是公平鎖,公平鎖就需要保證如果CLH隊列中有在排隊的線程,那么讓他們先獲取得到
                if ( !hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    // 設置為獨占
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果是當前的線程,那么表示的是可重入鎖。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

看一下是如何查看隊列中是否是有線程在排隊的:

    public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
    }

在這里來組成一個雙向鏈表,又稱之為CLH隊列。head指向隊頭,tail指向隊尾。

如果h!=t,表示的是CLH隊列中是有線程排隊的,后面的兩個判斷只要有一個判斷是成功的,那么就說明隊列中是有值的。

兩個判斷:1、如果頭結點的下一個節點為空;2、頭結點的下一個節點不為空並且不為當前線程;

如果返回true的話,那么表示CLH隊列中是有線程在排隊的;

如果是false的話,那么表示的是CLH隊列中是沒有線程在排隊的。

這里就是直接判斷是否存在首節點,head的節點的下一個節點(首節點)是有是有值的,如果有值,那么說明CLH隊列中是有值的。

如果隊列中沒有線程在等待鎖,可以看到利用CAS來獲取得到鎖,然后設置鎖被哪個線程獲取得到;

如果已經有線程持有了鎖,那么判斷是否是當前的線程,如果是,那么進行再次加鎖;

如果隊列中有線程在排隊等待鎖並且不是當前線程,那么直接獲取得到鎖失敗;

那么對應的流程如下所示:

對應的流程如下所示:

  • 1、每個線程在獲取鎖的時候,判斷鎖的狀態,如果是無鎖狀態,那么進入到隊列中查看隊列中是否有其它線程,如果沒有,那么去獲取得到鎖;如果有的話,那么獲取鎖事變,排隊等鎖;
  • 2、如果當前線程檢查是有鎖狀態,那么判斷持有鎖的是否是當前線程,如果是,那么鎖重入次數+1;
  • 3、如果不是當前線程持有鎖,那么獲取得到鎖失敗;

4.1、加鎖流程的兩種情況

總結起來,獲取得到鎖的線程只有兩種情況

1、當前鎖狀態是無鎖,且隊列中沒有線程在排隊,那么獲取得到鎖;

2、當前鎖狀態是有鎖,且持有鎖的線程是自己,那么這個時候鎖是可重入的;

5、線程沒有搶到鎖之后需要排隊

沒有搶到鎖的線程需要進行排隊,繼續看下源碼:

    public final void acquire(int arg) {
        // 沒有搶到鎖,返回false,取反為true,那么執行后面的邏輯
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先會創建線程等待者,當前是獨占模式

    private Node addWaiter(Node mode) {
        // 創建節點保存當前的線程
        Node node = new Node(Thread.currentThread(), mode);
        // 獲取得到尾結點
        Node pred = tail;
        if (pred != null) {
            // 如果尾結點不為空
            // 1、首先將尾節點的前驅指針指針尾指針指向的節點
            node.prev = pred;
            // 2、比較並交換,並將尾結點指針后移一位
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 這里有兩種情況存在。因為上面如果存在多線程競爭,稱為不了尾結點的線程將會走到這里來。看一下入隊操作
        // 1、tail為null;2、比較並交換稱為尾結點的節點
        enq(node);
        return node;
    }

注意看下上面的if判斷,在入隊尾的時進行比較並交換時,是失敗的,那么這個時候將會再次執行enq方法。

看一下enq方法:

    private Node enq(final Node node) {
        // 死循環
        for (;;) {
            Node t = tail;
            // 如果tail為null,那么這種也是上面的一種的情況。這里需要來進行初始化
            // 從這里也可以看到初始化的是一個空節點,不保存任何線程
            if (t == null) {
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 上面的另外一種情況。和上面的addWaiter中判斷是一致的
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

這里一直在使用死循環,這里存在着兩個作用:

  • 1、初始化,讓head和tail節點指向一個空的節點;

  • 2、將排隊的節點稱為尾結點(隊列特點:先進先出FIFO)

這里的for循環是為了構建CLH阻塞隊列。這里是第一個死循環隊列來進行構建的。

總之是為了將所有沒有搶到鎖的線程來進行入隊,這里的圖形畫出來:

這里就對應着上面的for循環操作。上面在死循環中構建一個CLH隊列,但是此時還沒有做任何操作,比如說節點中的值還沒有來得及對其進行設置。

6、CLH隊列中線程先搶鎖后阻塞

    final boolean acquireQueued(final Node node, int arg) {
        // 獲取鎖失敗為true
        boolean failed = true;
        try {
            // 線程中斷狀態
            boolean interrupted = false;
            for (;;) {
                // 獲取得到前驅節點。已經之前已經排好隊
                final Node p = node.predecessor();
                // 如果當前節點的頭結點是頭結點!那么再次來嘗試獲取得到一次鎖看看能不能成功
                // 因為可能在執行到這一步的時候,線程已經將鎖釋放了,所以這里再次來嘗試一下
                if (p == head && tryAcquire(arg)) {
                    // 將當前節點設置為頭結點
                    setHead(node);
                    // 當前的頭結點沒有作用了,需要GC掉
                    p.next = null; // help GC
                    // 因為搶占鎖成功,所以這里標注為fasle;
                    failed = false;
                    // 不是因為中斷引起的線程搶占鎖中斷
                    return interrupted;
                }
                // 在失敗獲取得到鎖的時候,應該將線程進行阻塞!這是才是重點!
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

6.1、for循環

這里的for循環是為了修改每個線程對應的節點的執行狀態的。只有節點狀態是SINGAL的時候才會在喚醒的時候有機會獲取得到線程。

而剛剛入隊的節點是並不是SINGAL狀態的,所以這里是在循環設置。那么看一下在失敗獲取得到鎖之后是如何將線程進行阻塞的:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 首先獲取得到每個線程排隊節點的前驅節點中的等待狀態!
        int ws = pred.waitStatus;
        // 如果是SINGAL,那么標識,就等着被喚醒來獲取得到鎖
        if (ws == Node.SIGNAL)
            return true;
        // 這里標識的是線程搶鎖取消,不再去搶鎖了
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
            // 如果是其他的,那么比較並交換,將當前的接地那的waitstatus進行設置
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

因為只有當這一步為true的時候,才會將線程進行阻塞。那么為true的就只有一步

if (ws == Node.SIGNAL)
    return true;

只有所有的節點中的status為Node.SIGNAL的時候,才會為true。

那么為fales的時候,將會再次走到下面的死循環中來:

            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    // 只有當線程阻塞過程中,是因為線程中斷而導致排隊中的線程被喚醒,那么這里標記
                    // 將會被設置為true;
                    interrupted = true;
            }

知道為true的時候,才會走到parkAndCheckInterrupt方法中來,而這一步是真正的做到將線程進行終止的操作的方法:

    private final boolean parkAndCheckInterrupt() {
        // 將當前線程阻塞到對象上來
        LockSupport.park(this);
        // 當前線程是否是以中斷引起的?如果是,那么返回true;如果不是,那么返回false;
        return Thread.interrupted();
    }

被park住的線程,此時被阻塞了,要是想蘇醒過來,必須要等到前一個線程來將其進行喚醒。

注意park()和park(this)的使用區別:

將隊列中的前驅節點中的waitstatus進行修改,表示的是可以將后來的線程來進行喚醒。

7、鎖釋放

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

直接看嘗試釋放鎖的代碼:

        protected final boolean tryRelease(int releases) {
            // 這里體現出現來可重入鎖的特性
            int c = getState() - releases;
            
            // 如果不是當前線程來釋放鎖,那么將會報錯!
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            // 將釋放標記來進行標記
            boolean free = false;
            // 如果c==0,那么表示的是鎖能夠釋放。如果是可重入鎖,那么這里不為0的時候,將會繼續來進行設置
            // 所以這里也就要求!加了多少次鎖,就要釋放多少次鎖
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            // 只有c==0的時候,這里才為true
            return free;
        }

那么再次回到上一步,成功釋放鎖之后操作

    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }

獲取得到頭結點,然后判斷頭節點不為空以及等待狀態不為0(必須是SIGNAL狀態)的時候,喚醒頭結點的下一個節點:

    private void unparkSuccessor(Node node) {
        // 獲取得到頭結點的狀態信息
        int ws = node.waitStatus;
        // 如果<0,那么比較並交換,將當前節點的狀態的status修改成0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 獲取得到下一個節點
        Node s = node.next;
        // 如果為空或者是等待狀態>0(明顯為0)
        if (s == null || s.waitStatus > 0) {
            // 失去引用,那么會GC掉
            s = null; 
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 喚醒線程
            LockSupport.unpark(s.thread);
    }

具體的執行圖如下所示:

當然,這里並沒有將將前驅節點斷掉,而是在喚醒線程后做的操作:

那么又再次回到原始起點:

            for (;;) {
                // 喚醒之后,又會來到這里
                final Node p = node.predecessor();
                // 嘗試獲取得到鎖。對於公平鎖來說,一定會執行到這一步,公平鎖一定會獲取得到
                if (p == head && tryAcquire(arg)) {
                    // 當前節點設置為頭結點,並消除掉前置指針
                    setHead(node);
                    // 后置指向置為空
                    p.next = null; // help GC
                    failed = false;
                    // 跳出循環
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }

看一下這里的setHead(node)方法:

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

將當前的節點設置為head節點,然后將節點中的線程置為空,然后斷掉前置指針。

最終的結果就是圖如下所示:

8、線程等待狀態補充說明

waitStatus 非常重要,也是關鍵,有下面幾個枚舉值:

枚舉 含義
CANCELLED 為1,表示線程獲取鎖的請求已經取消了
SIGNAL 為-1,表示線程已經准備好了,就等資源釋放了
CONDITION 為-2,表示節點在等待隊列中,節點線程等待喚醒
PROPAGATE 為-3,當前線程處在SHARED情況下,該字段才會使用
0 當一個Node被初始化的時候的默認值

至此公平鎖的流程分析結束:

lock.lock();
xxxx;
lock.unlock();

這段代碼的執行邏輯分析結束。

9、總結公平鎖的獲取流程

這里對應的是我自己畫的一個流程圖:

兩個for循環的作用:

  • 1、第一個for循環是排隊進入阻塞隊列隊尾;
  • 2、第二個for循環是修改每個節點的狀態;

隊頭喚醒之后進入循環

可以看到鎖在釋放的時候會喚醒下一個節點,喚醒下一個節點的時候,是線程自己進入到for循環中來再次嘗試獲取得到鎖。

對於公平鎖而言,隊頭元素肯定是可以獲取得到鎖的。因為有個判斷,判斷前驅節點是隊頭的才可以。

10、非公平鎖的加鎖流程

直接看對應的代碼:

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

之所以是非公平的,上來就進行比較並交換,如果成功,就比較並設置為當前的線程,野蠻至極。

就這一步的區別,其他的也沒有任何的變化了,所以總結起來只需要來畫個圖即可。

其實就是在線程剛剛進來的時候直接搶鎖,非常類似syncronized的流程。因為syncronzied鎖是非公平性的,也會嘗試搶占。

11、Lock鎖的特性講解

11.1、Lock鎖特性

  • 阻塞等待隊列
  • 共享鎖和獨占鎖(排他鎖)
  • 公平和非公平性
  • 可重入
  • 可中斷

11.2、Lock鎖是用變量state標識

用一個state標識來表示當前的鎖是否被占有,需要注意的是state變量是用volatile關鍵字來進行修飾的。

能夠及時讓其他線程線程看到鎖是否被搶占。

11.3、兩種隊列

  • 阻塞隊列
  • 條件隊列

阻塞對象是將沒有獲取得到鎖的線程放到CLH隊列中來進行阻塞;

條件隊列是在滿足條件的地方,調用condition.await方法的時候,將當前線程占有的鎖釋放掉,然后放入到條件隊列中來;當調用condition.sign()或者是condition.sinalAll()方法的時候,將被放在條件隊列中的線程追加到阻塞隊列上來,讓條件隊列中的線程有機會獲取得到鎖。

:條件隊列的使用及其類似多線程中原始的wait()/notify()/notifyAll()方法的使用

示例:

/**
 * @Description  等待喚醒機制  除了wait() 和notify\notifyAll方法而喚醒的
 *
 *              使用lock鎖的condition十分類似於notify和notifyAll的機制,只是通知,但是並沒有真正的將鎖給釋放掉
 *              而await方法,是將當前線程的執行權讓出去;讓當前的線程陷入到阻塞中去,等待其他線程的喚醒!
 *
 *              await在當前持有鎖的階段中釋放鎖,然后將自己放入到阻塞線程中去;
 *              sinal在持有鎖階段,將因為放到條件隊列中的線程喚醒,將條件隊列中的線程追加到阻塞隊列上去;
 * @Author liguang
 * @Date 2022/03/19/10:27
 */
public class LockTestOne {
    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        // 條件隊列
        Condition condition = lock.newCondition();

        new Thread(()->{
            lock.lock();
            try {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName+"---------開始處理任務");
                condition.await();
                System.out.println(threadName+"---------處理任務結束");
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                String threadName = Thread.currentThread().getName();
                System.out.println(threadName+"---------開始處理任務");
                condition.signal();
                System.out.println(threadName+"---------處理任務結束");
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }
        }).start();

    }
}

11.4、節點五種狀態

  • 值為0,初始化狀態,表示當前節點在sync隊列中,等待着獲取鎖。
  • CANCELLED,值為1,表示當前的線程被取消;
  • SIGNAL,值為-1,表示當前節點的后繼節點包含的線程需要運行,也就是unpark;
  • CONDITION,值為-2,表示當前節點在等待condition,也就是在condition隊列中;
  • PROPAGATE,值為-3,表示當前場景下后續的acquireShared能夠得以執行;

不同的自定義同步器競爭共享資源的方式也不同。自定義同步器在實現時只需要實現共享

資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出

隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:

isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現

它。

tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。

tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。

tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但

沒有剩余可用資源;正數表示成功,且有剩余資源。

tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續等待

結點返回true,否則返回false。

12、測試Lock鎖特性

12.1、可重入鎖

/**
 * 測試可重入性
 */
public class LockTestThree {
    public static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                method1();
            }).start();
        }
    }

    public static void method1(){
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"method1");
            method2();
        }finally {
            lock.unlock();
        }
    }

    private static void method2() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"method2");
            method3();
        }finally {
            lock.unlock();
        }
    }
    private static void method3() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName()+"method3");
        }finally {
            lock.unlock();
        }
    }
}

每次去獲取得到鎖的時候,都會發現占用的鎖的是當前的線程,所以會在state基礎之上+1。

問題:加了多少次鎖,就要釋放多少次鎖。不能多一次,否則將會導致一把鎖一直被一個線程一直占用。

示例:

public class ThreadLockCount {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                lock.lock();
                System.out.println("hello,world");
            }).start();
        }
    }
}

這里正是因為一個線程一直持有鎖(state),一直不為0,那么所有的線程都將會阻塞在隊列中,無法繼續向下繼續運行。

12.2、可中斷性

線程被喚醒有兩種情況

  • 鎖釋放,喚醒后續節點
  • 線程中斷

可以在源碼中的if判斷中,因為中斷而喚醒的,會有對應的中斷標記表示的是因為中斷而引起的線程中斷。

而給線程打上標記之后,在外部的某個地方,可以通過判斷線程狀態,來獲取得到對應的。對應的源碼體現:

   final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果是因為中斷引起的,那么會清除中斷標記並返回true
                    parkAndCheckInterrupt())
                    // 然后會給這個標識修改為true,表示是以為線程中斷引起的,外部可以做另外操作。
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

然后將標記暴露給外部:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

對應的狀態:

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

在外部程序可以檢測到這個異常信息。

來寫個代碼表示一下:

/**
 *
 * 100個線程來進行自增操作,但是其中一個線程在排隊時,發送中斷標記,告知該線程不應該繼續操作
 * 終止其當前線程正在運行的動作
 * @author liguang
 * @date 2022/7/28 10:00
 */
public class Test2 {

    private static final ReentrantLock lock = new ReentrantLock(false);

    private static int i = 0;

    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                reentrantLock();
            });
            threadList.add(thread);
            thread.start();
        }
        try {
            // 發一個中斷信號,將其進行喚醒
            threadList.get(90).interrupt();
            Thread.sleep(2000);
            System.out.println("最終確定的值是:"+i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void reentrantLock(){
        try {
            lock.lock();
            // 如果線程中斷了,就不應該再來進行后續的任務了
            while (Thread.currentThread().isInterrupted()){
                // 擦除掉中斷標記,然后返回,后面的事情不做了
                boolean interrupted = Thread.interrupted();
                System.out.println("線程標記裝填清除了"+interrupted);
                System.out.println(Thread.currentThread().isInterrupted());
            }
            i++;
        } catch (Exception e) {
            System.out.println(e);
            System.out.println("線程中斷了"+Thread.currentThread().getName());
            // 說明有一個線程是因為線程中斷喚醒的!所以需要將其進行替換掉
            System.out.println("當前線程狀態是:"+Thread.currentThread().isInterrupted());
        }finally {
            lock.unlock();
        }
    }

}

多運行幾次,會出現以下效果。因為線程運行過程中,如果中斷了,碰上了Thread.sleep的話,會導致異常出現。

因為沒有將線程睡眠一會兒,所以線程中斷可能在線程運行完成之后,也可能是在線程運行之前。

線程標記裝填清除了true
false
最終確定的值是:100

也有另外一個方法

lock.lockInterruptibly();

看看其實現原理:

    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 如果是因為線程中斷引起,那么直接拋出異常
                    throw new InterruptedException();
            }
        } finally {
            // 拋出異常之前,這里執行取消對應的排隊節點
            if (failed)
                cancelAcquire(node);
        }
    }

具體實例如下所示:

/**
 *
 * 100個線程來進行自增操作
 * @author liguang
 * @date 2022/7/28 10:00
 */
public class Test1 {

    private static final ReentrantLock lock = new ReentrantLock(false);

    private static int i = 0;

    public static void main(String[] args) {
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Thread thread = new Thread(() -> {
                reentrantLock();
            });
            threadList.add(thread);
            thread.start();
        }
        try {
            threadList.get(90).interrupt();
            Thread.sleep(1000);
            System.out.println("最終確定的值是:"+i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void reentrantLock(){
        try {
            lock.lockInterruptibly();
            Thread.sleep(10);
            i++;
        } catch (Exception e) {
            System.out.println("線程中斷了,拋出異常了");
        }finally {
            lock.unlock();
        }
    }

}

12.3.1、立即失敗

/**
 * 嘗試獲取得到鎖,這里是立即失敗,不管是公平鎖還是非公平鎖,都是理解返回的狀態
 */
public class LockTestFive {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("線程t1啟動");
            if (!lock.tryLock()){
                System.out.println("線程t1沒有獲取得到鎖,返回");
                return;
            }
            try {
                System.out.println("獲取得到了鎖!");
            }finally {
                // 將鎖釋放
                lock.unlock();
            }
        }, "lig");

        // main線程
        lock.lock();
        try {
            System.out.println("main線程獲取得到了鎖");
            t1.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }
}

12.3.2、超時失敗

/**
 * 在指定的時間內沒有獲取得到鎖之后,失敗
 */
public class LockTestSix {

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("線程t1啟動");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)){
                    System.out.println("線程t1沒有獲取得到鎖,返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 獲取得到鎖之后,下面的就不需要再來進行執行了
                return;
            }
            try {
                System.out.println("獲取得到了鎖!");
            }finally {
                // 將鎖釋放
                lock.unlock();
            }
        }, "lig");

        // main線程
        lock.lock();
        try {
            System.out.println("main線程獲取得到了鎖");
            t1.start();
            try {
                // 休眠兩秒鍾來進行測試
                Thread.sleep(2000);
                System.out.println("main線程釋放了鎖");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }finally {
            lock.unlock();
        }
    }
}

12.4、公平鎖和非公平鎖

/**
 * 嘗試獲取得到鎖,這里是立即失敗,不管是公平鎖還是非公平鎖,都是理解返回的狀態
 */
public class LockTestSeven {

    public static void main(String[] args) {
        // 嘗試公平鎖和非公平鎖
        // ReentrantLock lock = new ReentrantLock(true);
        ReentrantLock lock = new ReentrantLock();
        for (int i = 0; i < 5000; i++) {
            new Thread(()->{
                lock.lock();
                try {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("當前線程"+Thread.currentThread().getName()+" is running.........");
                }finally {
                    lock.unlock();
                }
            },"t"+i).start();
        }
        // 休眠之后再次去搶鎖
        try {
             Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 5000; i++) {
            new Thread(()->{
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+" is running.........");
                }finally {
                    lock.unlock();
                }
            },"強行搶鎖"+i).start();
        }
    }
}

讓控制台交替打印,可以看到非公平鎖是可以來交替打印對應的線程Name的。

12.5、條件變量

調用 Condition.await() 方法使線程等待,其他線程調用Condition.signal() 或 Condition.signalAll() 方法喚醒等待的線程。

注意:調用Condition的await()和signal()方法,都必須在lock保護之內

/**
 * 條件變量:模擬生產者和生產者!這是這里明顯,可以有多個條件,可以利用多個條件來進行操作
 */
public class LockTestEight {

    private static Lock lock = new ReentrantLock();
    private static Condition cigCon = lock.newCondition();
    private static Condition takeCon = lock.newCondition();
    private static boolean hasCig;
    private static boolean hasTakeOut;

    public void cigratee(){
        lock.lock();
        try {
            while (!hasCig){
                try {
                    System.out.println("沒有煙了,歇一會兒");
                    cigCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("有煙了,開始干活...........");
        }finally {
            lock.unlock();
        }
    }

    /**
     * 送外賣
     */
    public void takeOut(){
        lock.lock();
        try {
            while (!hasTakeOut){
                try {
                    System.out.println("飯還沒有好,歇一會兒");
                    takeCon.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("有飯了,吃完飯開始干活...........");
        }finally {
            lock.unlock();
        }
    }
    
    public static void main(String[] args) {
        LockTestEight lockTestEight = new LockTestEight();
        new Thread(()->{
            lockTestEight.cigratee();
        }).start();

        new Thread(()->{
            lockTestEight.takeOut();
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                hasCig = true;
                cigCon.signal();
            }finally {
                lock.unlock();
            }
        }).start();

        new Thread(()->{
            lock.lock();
            try {
                hasTakeOut = true;
                takeCon.signal();
            }finally {
                lock.unlock();
            }
        }).start();

    }
}

這個具體分析會在后面給列出來。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM