【Java并發(fā)】ReadWriteLock讀寫鎖的使用

說到Java并發(fā)編程缨叫,很多開發(fā)第一個想到同時也是經(jīng)常常用的肯定是Synchronized,但是小編這里提出一個問題钱床,Synchronized存在明顯的一個性能問題就是讀與讀之間互斥吼畏,簡言之就是,我們編程想要實現(xiàn)的最好效果是蛤签,可以做到讀和讀互不影響,讀和寫互斥栅哀,寫和寫互斥震肮,提高讀寫的效率,如何實現(xiàn)呢留拾?

Java并發(fā)包中ReadWriteLock是一個接口戳晌,主要有兩個方法,如下:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

ReadWriteLock管理一組鎖痴柔,一個是只讀的鎖钳垮,一個是寫鎖惜姐。
Java并發(fā)庫中ReetrantReadWriteLock實現(xiàn)了ReadWriteLock接口并添加了可重入的特性病蛉。
在具體講解ReetrantReadWriteLock的使用方法前汽烦,我們有必要先對其幾個特性進行一些深入學習了解鸿捧。

1. ReetrantReadWriteLock特性說明

1.1 獲取鎖順序

  • 非公平模式(默認)
    當以非公平初始化時屹篓,讀鎖和寫鎖的獲取的順序是不確定的。非公平鎖主張競爭獲取匙奴,可能會延緩一個或多個讀或寫線程堆巧,但是會比公平鎖有更高的吞吐量。
  • 公平模式
    當以公平模式初始化時泼菌,線程將會以隊列的順序獲取鎖谍肤。當當前線程釋放鎖后,等待時間最長的寫鎖線程就會被分配寫鎖哗伯;或者有一組讀線程組等待時間比寫線程長荒揣,那么這組讀線程組將會被分配讀鎖。

1.2 可重入

什么是可重入鎖系任,不可重入鎖呢恳蹲?"重入"字面意思已經(jīng)很明顯了,就是可以重新進入俩滥〖卫伲可重入鎖,就是說一個線程在獲取某個鎖后霜旧,還可以繼續(xù)獲取該鎖错忱,即允許一個線程多次獲取同一個鎖。比如synchronized內置鎖就是可重入的挂据,如果A類有2個synchornized方法method1和method2以清,那么method1調用method2是允許的。顯然重入鎖給編程帶來了極大的方便崎逃。假如內置鎖不是可重入的玖媚,那么導致的問題是:1個類的synchornized方法不能調用本類其他synchornized方法,也不能調用父類中的synchornized方法婚脱。與內置鎖對應今魔,JDK提供的顯示鎖ReentrantLock也是可以重入的,這里通過一個例子著重說下可重入鎖的釋放需要的事兒障贸。

package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {

    public static void main(String[] args) throws InterruptedException {
        final ReentrantReadWriteLock  lock = new ReentrantReadWriteLock ();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.writeLock().lock();
                System.out.println("Thread real execute");
                lock.writeLock().unlock();
            }
        });

        lock.writeLock().lock();
        lock.writeLock().lock();
        t.start();
        Thread.sleep(200);
        
        System.out.println("realse one once");
        lock.writeLock().unlock();
    }

}

運行結果.png

從運行結果中错森,可以看到,程序并未執(zhí)行線程的run方法篮洁,由此我們可知涩维,上面的代碼會出現(xiàn)死鎖,因為主線程2次獲取了鎖袁波,但是卻只釋放1次鎖瓦阐,導致線程t永遠也不能獲取鎖。一個線程獲取多少次鎖篷牌,就必須釋放多少次鎖睡蟋。這對于內置鎖也是適用的,每一次進入和離開synchornized方法(代碼塊)枷颊,就是一次完整的鎖獲取和釋放戳杀。
再次添加一次unlock之后的運行結果.png

1.3 鎖降級

要實現(xiàn)一個讀寫鎖,需要考慮很多細節(jié)夭苗,其中之一就是鎖升級和鎖降級的問題信卡。什么是升級和降級呢?ReadWriteLock的javadoc有一段話:

Can the write lock be downgraded to a read lock without allowing an intervening writer? Can a read lock be upgraded to a write lock, in preference to other waiting readers or writers?

翻譯過來的結果是:在不允許中間寫入的情況下题造,寫入鎖可以降級為讀鎖嗎傍菇?讀鎖是否可以升級為寫鎖,優(yōu)先于其他等待的讀取或寫入操作界赔?簡言之就是說丢习,鎖降級:從寫鎖變成讀鎖须妻;鎖升級:從讀鎖變成寫鎖,ReadWriteLock是否支持呢泛领?讓我們帶著疑問荒吏,進行一些Demo 測試代碼驗證。

Test Code 1

/**
 *Test Code 1
 **/
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test1 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
        rtLock.readLock().lock();
        System.out.println("get readLock.");
        rtLock.writeLock().lock();
        System.out.println("blocking");
    }
}

Test Code 1 Result

TestCode1 Result.png

結論:上面的測試代碼會產(chǎn)生死鎖渊鞋,因為同一個線程中绰更,在沒有釋放讀鎖的情況下,就去申請寫鎖锡宋,這屬于鎖升級儡湾,ReentrantReadWriteLock是不支持的

Test Code 2

/**
 *Test Code 2
 **/
package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Test2 {

    public static void main(String[] args) {
        ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();  
        rtLock.writeLock().lock();  
        System.out.println("writeLock");  
          
        rtLock.readLock().lock();  
        System.out.println("get read lock");  
    }
}

Test Code 2 Result

TestCode2 Result.png

結論:ReentrantReadWriteLock支持鎖降級执俩,上面代碼不會產(chǎn)生死鎖徐钠。這段代碼雖然不會導致死鎖,但沒有正確的釋放鎖役首。從寫鎖降級成讀鎖尝丐,并不會自動釋放當前線程獲取的寫鎖,仍然需要顯示的釋放衡奥,否則別的線程永遠也獲取不到寫鎖爹袁。

2. ReetrantReadWriteLock對比使用

2.1 Synchronized實現(xiàn)

在使用ReetrantReadWriteLock實現(xiàn)鎖機制前,我們先看一下矮固,多線程同時讀取文件時失息,用synchronized實現(xiàn)的效果

package test;

/**
 * 
 * synchronized實現(xiàn)
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public synchronized static void get(Thread thread) {
        System.out.println("start time:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在進行讀操作……");
        }
        System.out.println(thread.getName() + ":讀操作完畢!");
        System.out.println("end time:" + System.currentTimeMillis());
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();
    }

}

讓我們看一下運行結果:

synchronized實現(xiàn)的效果結果.png

從運行結果可以看出档址,兩個線程的讀操作是順序執(zhí)行的盹兢,整個過程大概耗時200ms。

2.2 ReetrantReadWriteLock實現(xiàn)

package test;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * ReetrantReadWriteLock實現(xiàn)
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void get(Thread thread) {
        lock.readLock().lock();
        System.out.println("start time:" + System.currentTimeMillis());
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(thread.getName() + ":正在進行讀操作……");
        }
        System.out.println(thread.getName() + ":讀操作完畢守伸!");
        System.out.println("end time:" + System.currentTimeMillis());
        lock.readLock().unlock();
    }

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                get(Thread.currentThread());
            }
        }).start();
    }

}

讓我們看一下運行結果:

ReetrantReadWriteLock實現(xiàn).png

從運行結果可以看出绎秒,兩個線程的讀操作是同時執(zhí)行的,整個過程大概耗時100ms含友。
通過兩次實驗的對比替裆,我們可以看出來校辩,ReetrantReadWriteLock的效率明顯高于Synchronized關鍵字窘问。

3. ReetrantReadWriteLock讀寫鎖互斥關系

通過上面的測試代碼,我們也可以延伸得出一個結論宜咒,ReetrantReadWriteLock讀鎖使用共享模式惠赫,即:同時可以有多個線程并發(fā)地讀數(shù)據(jù)。但是另一個問題來了故黑,寫鎖之間是共享模式還是互斥模式儿咱?讀寫鎖之間是共享模式還是互斥模式呢庭砍?下面讓我們通過Demo進行一一驗證吧。

3.1 ReetrantReadWriteLock讀寫鎖關系

package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * ReetrantReadWriteLock實現(xiàn)
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //同時讀混埠、寫
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                readFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }

    // 讀操作
    public static void readFile(Thread thread) {
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
            System.out.println("當前為讀鎖怠缸!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行讀操作……");
            }
            System.out.println(thread.getName() + ":讀操作完畢!");
        } finally {
            System.out.println("釋放讀鎖钳宪!");
            lock.readLock().unlock();
        }
    }

    // 寫操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("當前為寫鎖揭北!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行寫操作……");
            }
            System.out.println(thread.getName() + ":寫操作完畢!");
        } finally {
            System.out.println("釋放寫鎖吏颖!");
            lock.writeLock().unlock();
        }
    }
}

運行結果:

運行結果.png

結論:讀寫鎖的實現(xiàn)必須確保寫操作對讀操作的內存影響搔体。換句話說,一個獲得了讀鎖的線程必須能看到前一個釋放的寫鎖所更新的內容半醉,讀寫鎖之間為互斥疚俱。

3.2 ReetrantReadWriteLock寫鎖關系

package test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 
 * ReetrantReadWriteLock實現(xiàn)
 * @author itbird
 *
 */
public class ReadAndWriteLockTest {

    public static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        //同時寫
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                writeFile(Thread.currentThread());
            }
        });
    }

    // 讀操作
    public static void readFile(Thread thread) {
        lock.readLock().lock();
        boolean readLock = lock.isWriteLocked();
        if (!readLock) {
            System.out.println("當前為讀鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行讀操作……");
            }
            System.out.println(thread.getName() + ":讀操作完畢缩多!");
        } finally {
            System.out.println("釋放讀鎖呆奕!");
            lock.readLock().unlock();
        }
    }

    // 寫操作
    public static void writeFile(Thread thread) {
        lock.writeLock().lock();
        boolean writeLock = lock.isWriteLocked();
        if (writeLock) {
            System.out.println("當前為寫鎖!");
        }
        try {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(thread.getName() + ":正在進行寫操作……");
            }
            System.out.println(thread.getName() + ":寫操作完畢衬吆!");
        } finally {
            System.out.println("釋放寫鎖登馒!");
            lock.writeLock().unlock();
        }
    }
}

運行結果:


運行結果.png

4. 總結

1.Java并發(fā)庫中ReetrantReadWriteLock實現(xiàn)了ReadWriteLock接口并添加了可重入的特性
2.ReetrantReadWriteLock讀寫鎖的效率明顯高于synchronized關鍵字
3.ReetrantReadWriteLock讀寫鎖的實現(xiàn)中,讀鎖使用共享模式咆槽;寫鎖使用獨占模式陈轿,換句話說,讀鎖可以在沒有寫鎖的時候被多個線程同時持有秦忿,寫鎖是獨占的
4.ReetrantReadWriteLock讀寫鎖的實現(xiàn)中麦射,需要注意的,當有讀鎖時灯谣,寫鎖就不能獲得潜秋;而當有寫鎖時,除了獲得寫鎖的這個線程可以獲得讀鎖外胎许,其他線程不能獲得讀鎖

看完了使用方法峻呛,各位看官是否對于ReetrantReadWriteLock的實現(xiàn)原理有一探究竟的沖動呢?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末辜窑,一起剝皮案震驚了整個濱河市钩述,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌穆碎,老刑警劉巖牙勘,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異所禀,居然都是意外死亡方面,警方通過查閱死者的電腦和手機放钦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恭金,“玉大人操禀,你說我怎么就攤上這事『嵬龋” “怎么了床蜘?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蔑水。 經(jīng)常有香客問我邢锯,道長,這世上最難降的妖魔是什么搀别? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任丹擎,我火速辦了婚禮,結果婚禮上歇父,老公的妹妹穿的比我還像新娘蒂培。我一直安慰自己,他們只是感情好榜苫,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布护戳。 她就那樣靜靜地躺著,像睡著了一般垂睬。 火紅的嫁衣襯著肌膚如雪媳荒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天驹饺,我揣著相機與錄音钳枕,去河邊找鬼。 笑死赏壹,一個胖子當著我的面吹牛鱼炒,可吹牛的內容都是我干的。 我是一名探鬼主播蝌借,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼昔瞧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了菩佑?” 一聲冷哼從身側響起自晰,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎擎鸠,沒想到半個月后缀磕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡劣光,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年袜蚕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绢涡。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡牲剃,死狀恐怖,靈堂內的尸體忽然破棺而出雄可,到底是詐尸還是另有隱情凿傅,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布数苫,位于F島的核電站聪舒,受9級特大地震影響,放射性物質發(fā)生泄漏虐急。R本人自食惡果不足惜箱残,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望止吁。 院中可真熱鬧被辑,春花似錦、人聲如沸敬惦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俄删。三九已至宏怔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畴椰,已是汗流浹背举哟。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留迅矛,地道東北人妨猩。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像秽褒,于是被迫代替她去往敵國和親壶硅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容