讀寫鎖 ReentrantReadWriteLock
1. 前言
本節(jié)內(nèi)容主要是對(duì) Java 讀寫鎖 ReentrantReadWriteLock 進(jìn)行講解,本節(jié)內(nèi)容幾乎全部為重點(diǎn)知識(shí),需要學(xué)習(xí)者對(duì) ReentrantReadWriteLock 進(jìn)行理解和掌握。本節(jié)內(nèi)容的知識(shí)點(diǎn)如下:
- ReentrantReadWriteLock 簡(jiǎn)單介紹,對(duì) ReentrantReadWriteLock 進(jìn)行一個(gè)總體的概括;
- ReentrantReadWriteLock 的類結(jié)構(gòu),從 Java 層面了解 ReentrantReadWriteLock;
- ReentrantReadWriteLock 的特點(diǎn),相比于上兩點(diǎn)知識(shí),該知識(shí)點(diǎn)可視為重點(diǎn);
- ReentrantReadWriteLock 讀鎖共享的性質(zhì)驗(yàn)證,為本節(jié)核心內(nèi)容之一;
- ReentrantReadWriteLock 讀寫互斥的性質(zhì)驗(yàn)證,為本節(jié)核心內(nèi)容之一。
ReentrantReadWriteLock 在 Java 的鎖當(dāng)中也占據(jù)著十分重要的地位,在并發(fā)編程中使用頻率也是非常的高,一定要對(duì)本節(jié)內(nèi)容進(jìn)行細(xì)致的學(xué)習(xí)和掌握。
2. ReentrantReadWriteLock 介紹
JDK 提供了 ReentrantReadWriteLock 讀寫鎖,使用它可以加快效率,在某些不需要操作實(shí)例變量的方法中,完全可以使用讀寫鎖 ReemtrantReadWriteLock 來提升該方法的運(yùn)行速度。
定義:讀寫鎖表示有兩個(gè)鎖,一個(gè)是讀操作相關(guān)的鎖,也稱為共享鎖;另一個(gè)是寫操作相關(guān)的鎖,也叫排他鎖。
定義解讀:也就是多個(gè)讀鎖之間不互斥,讀鎖與寫鎖互斥、寫鎖與寫鎖互斥。在沒有線程 Thread 進(jìn)行寫入操作時(shí),進(jìn)行讀取操作的多個(gè) Thread 都可以獲取讀鎖,而進(jìn)行寫入操作的 Thread 只有在獲取寫鎖后才能進(jìn)行寫入操作。即多個(gè) Thread 可以同時(shí)進(jìn)行讀取操作,但是同一時(shí)刻只允許一個(gè) Thread 進(jìn)行寫入操作。
3. ReentrantReadWriteLock 的類結(jié)構(gòu)
ReentrantReadWriteLock 是接口 ReadWriteLock 的子類實(shí)現(xiàn),通過 JDK 的代碼可以看出這一實(shí)現(xiàn)關(guān)系。
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable{}
我們?cè)賮砜聪陆涌?ReadWriteLock,該接口只定義了兩個(gè)方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
通過調(diào)用相應(yīng)方法獲取讀鎖或?qū)戞i,可以如同使用 Lock 接口一樣使用。
4. ReentrantReadWriteLock 的特點(diǎn)
性質(zhì) 1 :可重入性。
ReentrantReadWriteLock 與 ReentrantLock 以及 synchronized 一樣,都是可重入性鎖,這里不會(huì)再多加贅述所得可重入性質(zhì),之前已經(jīng)做過詳細(xì)的講解。
性質(zhì) 2 :讀寫分離。
我們知道,對(duì)于一個(gè)數(shù)據(jù),不管是幾個(gè)線程同時(shí)讀都不會(huì)出現(xiàn)任何問題,但是寫就不一樣了,幾個(gè)線程對(duì)同一個(gè)數(shù)據(jù)進(jìn)行更改就可能會(huì)出現(xiàn)數(shù)據(jù)不一致的問題,因此想出了一個(gè)方法就是對(duì)數(shù)據(jù)加鎖,這時(shí)候出現(xiàn)了一個(gè)問題:
線程寫數(shù)據(jù)的時(shí)候加鎖是為了確保數(shù)據(jù)的準(zhǔn)確性,但是線程讀數(shù)據(jù)的時(shí)候再加鎖就會(huì)大大降低效率,這時(shí)候怎么辦呢?那就對(duì)寫數(shù)據(jù)和讀數(shù)據(jù)分開,加上兩把不同的鎖,不僅保證了正確性,還能提高效率。
性質(zhì) 3 :可以鎖降級(jí),寫鎖降級(jí)為讀鎖。
線程獲取寫入鎖后可以獲取讀取鎖,然后釋放寫入鎖,這樣就從寫入鎖變成了讀取鎖,從而實(shí)現(xiàn)鎖降級(jí)的特性。
性質(zhì) 4 :不可鎖升級(jí)。
線程獲取讀鎖是不能直接升級(jí)為寫入鎖的。需要釋放所有讀取鎖,才可獲取寫鎖。
5. ReentrantReadWriteLock 讀鎖共享
我們之前說過,ReentrantReadWriteLock 之所以優(yōu)秀,是因?yàn)樽x鎖與寫鎖是分離的,當(dāng)所有的線程都為讀操作時(shí),不會(huì)造成線程之間的互相阻塞,提升了效率,那么接下來,我們通過代碼實(shí)例進(jìn)行學(xué)習(xí)。
場(chǎng)景設(shè)計(jì):
- 創(chuàng)建三個(gè)線程,線程名稱分別為 t1,t2,t3,線程實(shí)現(xiàn)方式自行選擇;
- 三個(gè)線程同時(shí)運(yùn)行獲取讀鎖,讀鎖成功后打印線程名和獲取結(jié)果,并沉睡 2000 毫秒,便于觀察其他線程是否可共享讀鎖;
- finally 模塊中釋放鎖并打印線程名和釋放結(jié)果;
- 運(yùn)行程序,觀察結(jié)果。
結(jié)果預(yù)期:三條線程能同時(shí)獲取鎖,因?yàn)樽x鎖共享。
實(shí)例:
public class DemoTest {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 讀寫鎖
private int i;
public String readI() {
try {
lock.readLock().lock();// 占用讀鎖
System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用讀鎖,i->" + i);
Thread.sleep(2000);
} catch (InterruptedException e) {
} finally {
System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放讀鎖,i->" + i);
lock.readLock().unlock();// 釋放讀鎖
}
return i + "";
}
public static void main(String[] args) {
final DemoTest demo1 = new DemoTest();
Runnable runnable = new Runnable() {
@Override
public void run() {
demo1.readI();
}
};
new Thread(runnable, "t1"). start();
new Thread(runnable, "t2"). start();
new Thread(runnable, "t3"). start();
}
}
結(jié)果驗(yàn)證:
threadName -> t1 占用讀鎖,i->0
threadName -> t2 占用讀鎖,i->0
threadName -> t3 占用讀鎖,i->0
threadName -> t1 釋放讀鎖,i->0
threadName -> t3 釋放讀鎖,i->0
threadName -> t2 釋放讀鎖,i->0
結(jié)果分析:從結(jié)果來看,t1,t2,t3 均在同一時(shí)間獲取了鎖,證明了讀鎖共享的性質(zhì)。
6. ReentrantReadWriteLock 讀寫互斥
當(dāng)共享變量有寫操作時(shí),必須要對(duì)資源進(jìn)行加鎖,此時(shí)如果一個(gè)線程正在進(jìn)行讀操作,那么寫操作的線程需要等待。同理,如果一個(gè)線程正在寫操作,讀操作的線程需要等待。
場(chǎng)景設(shè)計(jì):細(xì)節(jié)操作不詳細(xì)闡述,看示例代碼即可。
- 創(chuàng)建兩個(gè)線程,線程名稱分別為 t1,t2;
- 線程 t1 進(jìn)行讀操作,獲取到讀鎖之后,沉睡 5000 毫秒;
- 線程 t2 進(jìn)行寫操作;
- 開啟 t1,1000 毫秒后開啟 t2 線程;
- 運(yùn)行程序,觀察結(jié)果。
結(jié)果預(yù)期:線程 t1 獲取了讀鎖,在沉睡的 5000 毫秒中,線程 t2 只能等待,不能獲取到鎖,因?yàn)樽x寫互斥。
實(shí)例:
public class DemoTest {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();// 讀寫鎖
private int i;
public String readI() {
try {
lock.readLock().lock();// 占用讀鎖
System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用讀鎖,i->" + i);
Thread.sleep(5000);
} catch (InterruptedException e) {
} finally {
System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放讀鎖,i->" + i);
lock.readLock().unlock();// 釋放讀鎖
}
return i + "";
}
public void addI() {
try {
lock.writeLock().lock();// 占用寫鎖
System.out.println("threadName -> " + Thread.currentThread().getName() + " 占用寫鎖,i->" + i);
i++;
} finally {
System.out.println("threadName -> " + Thread.currentThread().getName() + " 釋放寫鎖,i->" + i);
lock.writeLock().unlock();// 釋放寫鎖
}
}
public static void main(String[] args) throws InterruptedException {
final DemoTest demo1 = new DemoTest();
new Thread(new Runnable() {
@Override
public void run() {
demo1.readI();
}
}, "t1"). start();
Thread.sleep(1000);
new Thread(new Runnable() {
@Override
public void run() {
demo1.addI();
}
}, "t2"). start();
}
}
結(jié)果驗(yàn)證:
threadName -> t1 占用讀鎖,i->0
threadName -> t1 釋放讀鎖,i->0
threadName -> t2 占用寫鎖,i->0
threadName -> t2 釋放寫鎖,i->1
結(jié)果解析:驗(yàn)證成功,在線程 t1 沉睡的過程中,寫鎖 t2 線程無法獲取鎖,因?yàn)殒i已經(jīng)被讀操作 t1 線程占據(jù)了。
7. 小結(jié)
本節(jié)內(nèi)容只要是對(duì)讀寫鎖 ReentrantReadWriteLock 進(jìn)行的比較細(xì)致的講解,對(duì)于本節(jié)的內(nèi)容幾乎通篇為重點(diǎn)內(nèi)容。
其中核心知識(shí)點(diǎn)為讀鎖共享和讀寫互斥的驗(yàn)證,所有的知識(shí)點(diǎn)都是圍繞這兩個(gè)話題進(jìn)行講解的,有興趣的同學(xué)可以根據(jù)實(shí)例代碼進(jìn)行寫鎖互斥的驗(yàn)證。唯一不同的地方就是創(chuàng)建兩個(gè)寫線程進(jìn)行寫鎖的獲取。
掌握本節(jié)知識(shí)點(diǎn),有助于我們?cè)谔囟ǖ膱?chǎng)景下對(duì)讀寫鎖進(jìn)行應(yīng)用。