溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何解析Java多線程讀寫鎖ReentrantReadWriteLock類

發(fā)布時(shí)間:2021-12-18 10:45:47 來源:億速云 閱讀:146 作者:柒染 欄目:開發(fā)技術(shù)

這篇文章將為大家詳細(xì)講解有關(guān)如何解析Java多線程讀寫鎖ReentrantReadWriteLock類,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對相關(guān)知識有一定的了解。

    真實(shí)的多線程業(yè)務(wù)開發(fā)中,最常用到的邏輯就是數(shù)據(jù)的讀寫,ReentrantLock雖然具有完全互斥排他的效果(即同一時(shí)間只有一個(gè)線程正在執(zhí)行l(wèi)ock后面的任務(wù)),這樣做雖然保證了實(shí)例變量的線程安全性,但效率卻是非常低下的。所以在JDK中提供了一種讀寫鎖ReentrantReadWriteLock類,使用它可以加快運(yùn)行效率。

    讀寫鎖表示兩個(gè)鎖,一個(gè)是讀操作相關(guān)的鎖,稱為共享鎖;另一個(gè)是寫操作相關(guān)的鎖,稱為排他鎖。

    下面我們通過代碼去驗(yàn)證下讀寫鎖之間的互斥性

    ReentrantReadWriteLock

    讀讀共享

    首先創(chuàng)建一個(gè)對象,分別定義一個(gè)加讀鎖方法和一個(gè)加寫鎖的方法,

    public class MyDomain3 {
     
        private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
     
        public void testReadLock() {
            try {
                lock.readLock().lock();
                System.out.println(System.currentTimeMillis() + " 獲取讀鎖");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
            }
        }
     
        public void testWriteLock() {
            try {
                lock.writeLock().lock();
                System.out.println(System.currentTimeMillis() + " 獲取寫鎖");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
            }
        }
     
    }

    創(chuàng)建線程類1 調(diào)用加讀鎖方法

    public class Mythread3_1 extends Thread {
     
        private MyDomain3 myDomain3;
     
        public Mythread3_1(MyDomain3 myDomain3) {
            this.myDomain3 = myDomain3;
        }
     
        @Override
        public void run() {
            myDomain3.testReadLock();
        }
    }
    @Test
        public void test3() throws InterruptedException {
            MyDomain3 myDomain3 = new MyDomain3();
            Mythread3_1 readLock = new Mythread3_1(myDomain3);
            Mythread3_1 readLock2 = new Mythread3_1(myDomain3);
        readLock.start();
        readLock2.start();
     
            Thread.sleep(3000);
        }

    執(zhí)行結(jié)果:

    1639621812838 獲取讀鎖
    1639621812839 獲取讀鎖

    可以看出兩個(gè)讀鎖幾乎同時(shí)執(zhí)行,說明讀和讀之間是共享的,因?yàn)樽x操作不會有線程安全問題。

    寫寫互斥

    創(chuàng)建線程類2,調(diào)用加寫鎖方法

    public class Mythread3_2 extends Thread {
     
        private MyDomain3 myDomain3;
     
        public Mythread3_2(MyDomain3 myDomain3) {
            this.myDomain3 = myDomain3;
        }
     
        @Override
        public void run() {
            myDomain3.testWriteLock();
        }
    }
    @Test
        public void test3() throws InterruptedException {
            MyDomain3 myDomain3 = new MyDomain3();
            Mythread3_2 writeLock = new Mythread3_2(myDomain3);
            Mythread3_2 writeLock2 = new Mythread3_2(myDomain3);
     
            writeLock.start();
            writeLock2.start();
     
            Thread.sleep(3000);
        }

    執(zhí)行結(jié)果:

    1639622063226 獲取寫鎖
    1639622064226 獲取寫鎖

    從時(shí)間上看,間隔是1000ms即1s,說明寫鎖和寫鎖之間互斥。

    讀寫互斥

    再用線程1和線程2分別調(diào)用讀鎖與寫鎖

    @Test
        public void test3() throws InterruptedException {
            MyDomain3 myDomain3 = new MyDomain3();
            Mythread3_1 readLock = new Mythread3_1(myDomain3);
            Mythread3_2 writeLock = new Mythread3_2(myDomain3);
     
        readLock.start();
            writeLock.start();
     
            Thread.sleep(3000);
        }

    執(zhí)行結(jié)果:

    1639622338402 獲取讀鎖
    1639622339402 獲取寫鎖

    從時(shí)間上看,間隔是1000ms即1s,和代碼里面是一致的,證明了讀和寫之間是互斥的。

    注意一下,"讀和寫互斥"和"寫和讀互斥"是兩種不同的場景,但是證明方式和結(jié)論是一致的,所以就不證明了。

    最終測試結(jié)果下:

    • 1、讀和讀之間不互斥,因?yàn)樽x操作不會有線程安全問題

    • 2、寫和寫之間互斥,避免一個(gè)寫操作影響另外一個(gè)寫操作,引發(fā)線程安全問題

    • 3、讀和寫之間互斥,避免讀操作的時(shí)候?qū)懖僮餍薷牧藘?nèi)容,引發(fā)線程安全問題

    總結(jié)起來就是,多個(gè)Thread可以同時(shí)進(jìn)行讀取操作,但是同一時(shí)刻只允許一個(gè)Thread進(jìn)行寫入操作。

    源碼分析

    讀寫鎖中的Sync也是同樣實(shí)現(xiàn)了AQS,回想ReentrantLock中自定義同步器的實(shí)現(xiàn),同步狀態(tài)表示鎖被一個(gè)線程重復(fù)獲取的次數(shù),而讀寫鎖的自定義同步器需要在同步狀態(tài)(一個(gè)整型變量)上維護(hù)多個(gè)讀線程和一個(gè)寫線程的狀態(tài),使得該狀態(tài)的設(shè)計(jì)成為讀寫鎖實(shí)現(xiàn)的關(guān)鍵。

    讀寫鎖將變量切分成了兩個(gè)部分,高16位表示讀,低16位表示寫

    如何解析Java多線程讀寫鎖ReentrantReadWriteLock類

    當(dāng)前同步狀態(tài)表示一個(gè)線程已經(jīng)獲取了寫鎖,且重進(jìn)入了兩次,同時(shí)也連續(xù)獲取了兩次讀鎖。讀寫鎖是如何迅速確定讀和寫各自的狀態(tài)呢?

    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
     
    /** Returns the number of shared holds represented in count  */
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    /** Returns the number of exclusive holds represented in count  */
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

    其實(shí)是通過位運(yùn)算。假設(shè)當(dāng)前同步狀態(tài)值為c,寫狀態(tài)等于c & EXCLUSIVE_MASK (c&0x0000FFFF(將高16位全部抹去)),讀狀態(tài)等于c>>>16(無符號補(bǔ)0右移16位)。當(dāng)寫狀態(tài)增加1時(shí),等于c+1,當(dāng)讀狀態(tài)增加1時(shí),等于c+(1<<16),也就是c+0x00010000。

    根據(jù)狀態(tài)的劃分能得出一個(gè)推論:c不等于0時(shí),當(dāng)寫狀態(tài)(c & 0x0000FFFF)等于0時(shí),則讀狀態(tài)(c>>>16)大于0,即讀鎖已被獲取。

    寫鎖的獲取與釋放

    通過上面的測試,我們知道寫鎖是一個(gè)支持重入的排它鎖,看下源碼是如何實(shí)現(xiàn)寫鎖的獲取

    protected final boolean tryAcquire(int acquires) {
                /*
                 * Walkthrough:
                 * 1. If read count nonzero or write count nonzero
                 *    and owner is a different thread, fail.
                 * 2. If count would saturate, fail. (This can only
                 *    happen if count is already nonzero.)
                 * 3. Otherwise, this thread is eligible for lock if
                 *    it is either a reentrant acquire or
                 *    queue policy allows it. If so, update state
                 *    and set owner.
                 */
                Thread current = Thread.currentThread();
                int c = getState();
                int w = exclusiveCount(c);
                if (c != 0) {
                    // (Note: if c != 0 and w == 0 then shared count != 0)
                    if (w == 0 || current != getExclusiveOwnerThread())
                        return false;
                    if (w + exclusiveCount(acquires) > MAX_COUNT)
                        throw new Error("Maximum lock count exceeded");
                    // Reentrant acquire
                    setState(c + acquires);
                    return true;
                }
                if (writerShouldBlock() ||
                    !compareAndSetState(c, c + acquires))
                    return false;
                setExclusiveOwnerThread(current);
                return true;
            }

    第3行到第11行,簡單說了下整個(gè)方法的實(shí)現(xiàn)邏輯,這里要夸一下,這段注釋就很容易的讓人知道代碼的功能。下面我們分析一下,第13到第15行,分別拿到了當(dāng)前線程對象current,lock的加鎖狀態(tài)值c 以及寫鎖的值w,c!=0 表明 當(dāng)前處于有鎖狀態(tài),再繼續(xù)分析第16行到25行,有個(gè)關(guān)鍵的Note:(Note: if c != 0 and w == 0 then shared count != 0):簡單說就是:如果一個(gè)有鎖狀態(tài)但是沒有寫鎖,那么肯定加了讀鎖。

    第18行if條件,就是判斷加了讀鎖,但是當(dāng)前線程不是鎖擁有的線程,那么獲取鎖失敗,證明讀寫鎖互斥。

    第20行到第25行,走到這步,說明 w !=0 ,已經(jīng)獲取了寫鎖,只要不超過寫鎖最大值,那么增加寫狀態(tài)然后就可以成功獲取寫鎖。

    如果代碼走到第26行,說明c==0,當(dāng)前沒有加任何鎖,先執(zhí)行 writerShouldBlock()方法,此方法用來判斷寫鎖是否應(yīng)該阻塞,這塊是對公平與非公平鎖會有不同的邏輯,對于非公平鎖,直接返回false,不需要阻塞,下面是公平鎖執(zhí)行的判斷

    public final boolean hasQueuedPredecessors() {
            // The correctness of this depends on head being initialized
            // before tail and on head.next being accurate if the current
            // thread is first in queue.
            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());
        }

    對于公平鎖需要判斷當(dāng)前等待隊(duì)列中是否存在 等于當(dāng)前線程并且正在排隊(duì)等待獲取鎖的線程。

    寫鎖的釋放與ReentrantLock的釋放過程基本類似,每次釋放均減少寫狀態(tài),當(dāng)寫狀態(tài)為0時(shí)表示寫鎖已被釋放,從而等待的讀寫線程能夠繼續(xù)訪問讀寫鎖,同時(shí)前次寫線程的修改對后續(xù)讀寫線程可見。

    讀鎖的獲取與釋放

    讀鎖是一個(gè)支持重進(jìn)入的共享鎖,它能夠被多個(gè)線程同時(shí)獲取。JDK源碼如下:

    protected final int tryAcquireShared(int unused) {
                Thread current = Thread.currentThread();
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return -1;
                int r = sharedCount(c);
                if (!readerShouldBlock() &&
                    r < MAX_COUNT &&
                    compareAndSetState(c, c + SHARED_UNIT)) {
                    if (r == 0) {
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                        firstReaderHoldCount++;
                    } else {
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return 1;
                }
                return fullTryAcquireShared(current);
            }

    第4行到第6行,如果寫鎖被其他線程持有,則直接返回false,獲取讀鎖失敗,證明不同線程間寫讀互斥。

     第8行,readerShouldBlock() 獲取讀鎖是否應(yīng)該阻塞,這兒也同樣要區(qū)分公平鎖和非公平鎖,公平鎖模式需要判斷當(dāng)前等待隊(duì)列中是否存在 等于當(dāng)前線程并且正在排隊(duì)等待獲取鎖的線程,存在則獲取讀鎖需要等待。

    非公平鎖模式需要判斷當(dāng)前等待隊(duì)列中第一個(gè)是等待寫鎖的,則方法返回true,獲取讀鎖需要等待。

    fullTryAcquireShared() 主要是處理讀鎖獲取的完整版本,它處理tryAcquireShared()中沒有處理的CAS錯(cuò)誤和可重入讀鎖的處理邏輯。

    關(guān)于如何解析Java多線程讀寫鎖ReentrantReadWriteLock類就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI