理解J.U.C中的ReentrantLock

好一個迷途小書童發表於2021-01-03

JUC是啥?

其實很簡單,大家都喜歡縮寫!J.U.C= java.util.concurrent就是這個東西

來自哪裡?出現的理由

在Lock介面出現之前,java中的應用程式對於多執行緒的併發安全處理只能基於synchronized關鍵字來解決。但是synchronized在有些場景中會存在一些短板,也就是它並不適合所有的併發場景。但是在java5以後,Lock的出現可以解決synchronized在某些場景中的短板,它比synchronized更加靈活

下面我們來簡單介紹幾種鎖:

  • 1、ReentrantLock(重入鎖)
  • 2、ReentrantReadWriteLock(重入讀寫鎖)

看下面的案例: ReentrantLock的Demo

public class ReentrantLockTest1 {


    static int value = 0;


    Lock lock = new ReentrantLock();


    public static void incr() {


        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        value++;
    }


    public static void main(String[] args) {




        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 啟動執行緒
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        System.out.println("value的值為:" + value);
    }
}

結果: value的值為:960

很明顯這個結果不是我們想要的!我們想要的是: 1000

繼續往下看:

public class ReentrantLockTest1 {


    static int value = 0;


    static Lock lock = new ReentrantLock();


    public static void incr() {


        try {
            lock.lock();
            value ++;
            try {


                Thread.sleep(1);
                value++;
            } catch (InterruptedException e) {


                e.printStackTrace();


            }
        } finally {


            lock.unlock();
        }
    }




    public static void main(String[] args) {




        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 啟動執行緒
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        System.out.println("value的值為:" + value);
    }
}

結果: value的值為:89
說明什麼?完整獲取鎖的執行只有89次,我們在改變一下

接著看下面的案例:

public class ReentrantLockTest1 {


    static int value = 0;


    static Lock lock = new ReentrantLock();


    public static void incr() {


        try {
            lock.lock();
            value++;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 啟動執行緒
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        Thread.sleep(3000);
        System.out.println("value的值為:" + value);
    }
}

結果: value的值為:1000

以上得出的結論是: ReentrantLock.lock() 確實可以保證多執行緒情況下的執行緒安全,前提是你得讓他執行完!

在上面執行的工程中我們發現一個問題我們嘗試過用ReentrantLock.tryLock() 去嘗試獲得鎖,但是存在一個問題:

public class ReentrantLockTest1 {


    static int value = 0;


    static Lock lock = new ReentrantLock();


    public static void incr() {


        if (lock.tryLock()) {
            value++;
            lock.unlock();
        }
    }


    public static void main(String[] args) throws InterruptedException {




        Thread[] threads = new Thread[1000];


        for (int i = 0; i < 1000; i++) {


            threads[i] = new Thread(() -> {
                incr();
            });
        }
        // 啟動執行緒
        for (int i = 0; i < 1000; i++) {


            threads[i].start();
        }


        Thread.sleep(10000);
        System.out.println("value的值為:" + value);
    }
}

前提: 我試過把睡眠時間調整為 37710秒,但是得到的結果都是不足1000
     這樣子說來
     ReentrantLock.lock()
     ReentrantLock.tryLock()
     存在很大區別了
    
    從結果上看:ReentrantLock.lock()最起碼能保證結果的正確性
              ReentrantLock.tryLock()不能保證結果的正確性
    我們先去看下ReentrantLock.tryLock()因為Lock()的底層原理我已經比較熟悉了
              程式碼如下:

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}


ReentrantLock.lock()最起碼能保證結果的正確性的原因是:

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

它將為獲取到鎖的執行緒放置到了一個等待佇列(雙向連結串列)中

所以lock() tryLock() 從本質上講還是存在很大區別的!!!

下面我們再說下: ReentrantReadWriteLock(重入讀寫鎖)
看下面的案例:

public class Demo {


    static Map<String, Object> cacheMap = new HashMap<>();


    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();


    static Lock read = rwl.readLock();


    static Lock write = rwl.writeLock();


    static Lock fromLock = new ReentrantLock();


    public static Object get(String key) {

        if (fromLock.tryLock())
            // 讀鎖  阻塞
            read.lock();


        try {

            return cacheMap.get(key);

        } finally {

            read.unlock();
        }
    }


    public static Object write(String key, Object value) {


        // other thread  獲得了寫鎖
        write.lock();


        try {
            return cacheMap.put(key, value);
        } finally {
            write.unlock();
        }
    }
}

說明: 當多個執行緒訪問get()/write()方法的時候,當多個執行緒讀一個變數的時候是不互斥的,但是當一個執行緒獲取了寫鎖,那麼此時
      讀鎖會阻塞,防止拿到當資料

Ps: ReentrantReadWriteLock適用於讀多寫少的場景

但是!究竟尼瑪為啥,當獲取寫鎖的時候讀鎖會阻塞?我們去看看

/**
 * A {@code ReadWriteLock} maintains a pair of associated {@link
 * Lock locks}, one for read-only operations and one for writing.
 * The {@link #readLock read lock} may be held simultaneously by
 * multiple reader threads, so long as there are no writers.  The
 * {@link #writeLock write lock} is exclusive.
*/

我感覺已經說的很明顯了。。實際上是因為位置,沒有看到具體的實現

上面的問題呢?先放著吧,暫時超出我的能力,需要指引!!!

思考鎖的實現(設計思維)

1、鎖的互斥
2、沒有搶佔到鎖的執行緒?
3、等待的執行緒怎麼儲存?
4、公平和非公平(能否插隊)
5、重入的特性(識別是否同一個人?ThreadID)


解決方案:

1、鎖的互斥,說的在簡單點就是就共享資源的競爭,巧的是以前搶奪的是共享資源!現在搶佔的是一個標誌位!state,如果state=0那麼代表當前執行緒沒有搶佔到鎖,如果state=1則代表搶佔到了鎖,可以繼續向下執行

23 沒有搶佔到鎖的執行緒我們該如何處理?等待的執行緒怎麼儲存?我們可以舉例下面的一個場景,好比去醫院看病,這個例子不好!換一個~假如我們去洗腳城洗腳吧,我們中意7號!但是奈何喜歡她的人比較多,老闆只能讓你等著等7號空閒出來了,你才能上!用詞錯誤,你才能洗~ 但是,不可能說我先來的我最後一個上是吧,所以老闆需要給我發一個號碼牌,假定是9527號,按照正常來講一定是順序排隊的,誰先來,誰上!

4、這個公平不公平我們沿用上面的例子!正常來說一定是誰先來的誰先上,但是存在一個問題,一個新來的大哥,看隊伍比較長,他想先洗,不洗就掛了!拿500塊買我的位置~ 我可能也不會賣,除非給我550!如果我賣他了,那就是不公平的(大哥插隊了),如果我大喝一聲: 這世道竟然還有插隊的!?他可能就得老老實實排隊去了,那麼就是公平的,因為得排隊

5、重入性這個就比較有意思了~ 7號給大爺,再加個鍾!!,懂的都懂。。不能再說了

技術方案:

1volatile state = 0;(無鎖)1代表是持有鎖, > 1代表重入
2、wait/notify馬上到!condition 需要喚醒指定執行緒。【LockSupport.park(); -> unpark(thread)】 unsafe類中提供的一個方法
3、雙向連結串列
4、邏輯層面實現
5、在某一個地方儲存當前獲得鎖的執行緒的ID,判斷下次搶佔鎖的執行緒是否為同一個


下面我們來模擬一個場景: 模擬三個執行緒爭奪lock()的場景(先把總體的圖給你們,再去看原始碼分析)

在這裡插入圖片描述


/**
 * Acquires the lock.
 *
 * <p>If the lock is not available then the current thread becomes
 * disabled for thread scheduling purposes and lies dormant until the
 * lock has been acquired.
 *
 * <p><b>Implementation Considerations</b>
 *
 * <p>A {@code Lock} implementation may be able to detect erroneous use
 * of the lock, such as an invocation that would cause deadlock, and
 * may throw an (unchecked) exception in such circumstances.  The
 * circumstances and the exception type must be documented by that
 * {@code Lock} implementation.
 */

void lock();

說的什麼意思呢?

1、嘗試獲取鎖
2、在獲取鎖的過程中如果發現當前鎖沒搶到那麼,當前執行緒會變為阻塞狀態進入休眠狀態
3、當持有鎖的執行緒釋放掉鎖,那麼休眠的執行緒就可以去競爭鎖


/**
 * Acquires the lock.
 *
 * <p>Acquires the lock if it is not held by another thread and returns
 * immediately, setting the lock hold count to one.
 *
 * <p>If the current thread already holds the lock then the hold
 * count is incremented by one and the method returns immediately.
 *
 * <p>If the lock is held by another thread then the
 * current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the lock has been acquired,
 * at which time the lock hold count is set to one.
 */
public void lock() {
    sync.lock();
}

這個是ReentrantLock裡面的lock()說的什麼意思呢?

1、如果當前鎖沒有被持有那麼當前執行緒持有鎖,並且將持有次數設定為1
2、如果當前執行緒已經持有了鎖,那麼持有次數 + 1,並且立即返回表示持有鎖
3、同上

這個sync是啥?瞅一瞅

提供所有實現機制的同步器,基於AQS去表示當前鎖的狀態,成吧(我是沒理解)
說下我的理解吧

保證鎖狀態"state"的實時性,這東西就是幹這個的!

我們接著看非公平鎖的實現


/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;


    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }


    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

分析: (我們多分析點,多個執行緒搶奪鎖的情況,分析如圖的情況吧ThreadA、ThreadB、ThreadC)


第一次,剛剛進入,此時state = 0, 那麼我們進入if分支

setExclusiveOwnerThread(Thread.currentThread());

註釋如下:
/**
 * Sets the thread that currently owns exclusive access.
 * A {@code null} argument indicates that no thread owns access.
 * This method does not otherwise impose any synchronization or
 * {@code volatile} field accesses.
 * @param thread the owner thread
 */
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

/**
 * The current owner of exclusive mode synchronization.
 */
private transient Thread exclusiveOwnerThread;

解釋:

它說了一堆沒有的沒用的,總結就是一句話: 表示當前這個執行緒擁有了鎖,可以去訪問了!沒了。

總結: 第一次進入做了什麼事呢?

1、設定state 0 ---> 1
2、設定exclusiveOwnerThread 為當前執行緒
(我畫的圖還是蠻好的!!!)

那麼當一個執行緒持有鎖,其他執行緒進入是什麼樣子的一個情況呢?我們繼續分析


它會進入else分支,那麼如下:

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */

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

註解解釋:

忽略打斷,至少呼叫一次tryAcquire(),嘗試去獲取鎖
換句話說,執行緒在一個佇列中可能被再次阻塞和釋放,不斷呼叫tryAcquire()
方法直到成功,該方法被呼叫一般在實現了Lock介面(聽不出什麼東西),不過可以知曉下面兩點:

1、阻塞的執行緒在佇列中
2、阻塞的執行緒會呼叫tryAcquire()方法


我們再來仔細分析下acquire(int arg),這裡面呼叫了什麼方法,呵~好傢伙,可不少

1tryAcquire(arg)
2addWaiter(Node.EXCLUSIVE)
3acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

三個方法,我們一個一個來分析

1tryAcquire(arg),其實在分析它前我們可以猜一下這個方法幹了什麼?

    A、檢視當前的state是否變為了0,如果為零了,那麼就返回

養成好習慣,看原始碼前要先讀註釋,要先在總體上有一個把握,再去看具體的實現,不然,你看個什麼玩意,聽話養成好習慣,別看一大串子,別急,原始碼急不來的
差距就是在一點一滴中養成的

/**
 * Attempts to acquire in exclusive mode. This method should query
 * if the state of the object permits it to be acquired in the
 * exclusive mode, and if so to acquire it.
 *
 * <p>This method is always invoked by the thread performing
 * acquire.  If this method reports failure, the acquire method
 * may queue the thread, if it is not already queued, until it is
 * signalled by a release from some other thread. This can be used
 * to implement method {@link Lock#tryLock()}.
 *
 * <p>The default
 * implementation throws {@link UnsupportedOperationException}.
 *
 * @param arg the acquire argument. This value is always the one
 *        passed to an acquire method, or is the value saved on entry
 *        to a condition wait.  The value is otherwise uninterpreted
 *        and can represent anything you like.
 * @return {@code true} if successful. Upon success, this object has
 *         been acquired.
 * @throws IllegalMonitorStateException if acquiring would place this
 *         synchronizer in an illegal state. This exception must be
 *         thrown in a consistent fashion for synchronization to work
 *         correctly.
 * @throws UnsupportedOperationException if exclusive mode is not supported
 */
protected boolean tryAcquire(int arg) {

    throw new UnsupportedOperationException();
}

我們來一起讀,其實我也沒看過這裡,也是新的知識,這是我的學習方法,我感覺還不錯吧


1、嘗試去獲取獨佔模式(也就是去獲取這個鎖)
2、當state 准許被訪問的時候,訪問這個方法的執行緒應該是有序的排隊訪問
3、如果說執行緒沒有獲取到state那麼它可能會進等待佇列中,如果它沒有在等待佇列中話(這裡面是有說法的 a、等待佇列中的執行緒去順序獲取state b、未在佇列中的也可以競爭)
4、以上的所有前提是: signalled by a release(state)

Ps: 其實說的已經很明顯了!你看我們上面的圖,沒有獲取到鎖的執行緒,它會進入到一個雙向的等待佇列中

繼續往下看:


/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

這個方法比較簡單:
其實,這個方法要明確一個前提就是,我們可以嘗試著去獲取鎖了!(此時鎖可能還未釋放)

1、如果搶佔到了則獲取state,並設定執行緒為自己
2、如果獲取state的執行緒為當前持有state的執行緒,那麼重入次數 + 1


下面我們來分析第二個方法: addWaiter(Node.EXCLUSIVE), arg)


這個中規中矩,其實還可以吧

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

解釋:

新增節點至雙向佇列,節點以給定的模式進行儲存,如果當前佇列存在節點,那麼進入if分支,如果不存在節點那麼走非if分支,我們接著看這兩個分支

我們這個先進入enq(node);這個方法

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

解釋: 可以看出以下知識點:

1、尾插法  Node t = tail
2、當佇列中不存在元素的時候那麼tail = head = new Node
3else 分支node.prev = t其實執行的操作就是新插入元素的前一個元素為原佇列的尾節點,那麼可以判斷
   新插入的元素必定為佇列的尾節點
4、我們看下compareAndSetTail(t, node),應該指的就是我們上面的操作,點進去之後發現是一個native方法,但是可以推測和我們猜測差不多的
5compareAndSetHead(new Node()) 這個方法點進去也是native的至於功能我們也闡述過了

Ps: 再來看下我們的圖: 沒有獲得鎖的執行緒,是不是很神奇


我們接著往下看,第三個方法: 是以第二個方法返回的Node作為引數

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
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) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


解釋下:
通俗點解釋就是,將這個未獲取到鎖的Node丟到等待佇列中,當鎖可以被競爭了"state"那麼他就活了

/**
 * Returns previous node, or throws NullPointerException if null.
 * Use when predecessor cannot be null.  The null check could
 * be elided, but is present to help the VM.
 *
 * @return the predecessor of this node
 */
final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
        throw new NullPointerException();
    else
        return p;
}

返回當前節點的前一個節點

繼續研究: final boolean acquireQueued(final Node node, int arg)

那麼如下: 說了啥呢

1、如果當前節點的前一個節點為頭結點並且嘗試獲取鎖成功!那麼將node設定為當前等待佇列的head節點
2、如果不成立的話,說明當前鎖還是不可獲取的狀態這時判斷是否可以掛起當前執行緒、
3、如果判斷結果為真則掛起當前執行緒, 否則繼續迴圈, 
4、在這期間執行緒不響應中斷

5、在最後確保如果獲取失敗就取消獲取

       if (failed) {
           cancelAcquire(node);
       }

我目前的水平值准許我分析到這種程度了。。以後找到物件我再繼續分析,哈哈! 再見。

有問題,大家一起討論,不開心你罵我也成,但是你得說出所以然,不然我可能會去打你。。

圖感覺有點花,可以看這個: https://app.yinxiang.com/fx/279855bd-bcda-462e-be8f-e69ab987df95

相關文章