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