一、摘要
在上篇文章中,我們講到ReentrantLock
可以保證了只有一個線程能執行加鎖的代碼。
但是有些時候,這種保護顯的有點過頭,比如下面這個方法,它僅僅就是隻讀取數據,不修改數據,它實際上允許多個線程同時調用的。
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public int get() {
// 加鎖
lock.lock();
try {
return count;
} finally {
// 釋放鎖
lock.unlock();
}
}
}
站在程序性能的角度,實際上我們想要的是這樣的效果。
- 1.讀和讀之間不互斥,因爲只讀操作不會有數據安全問題
- 2.寫和寫之間互斥,避免一個寫操作影響另外一個寫操作,引發數據計算錯誤問題
- 3.讀和寫之間互斥,避免讀操作的時候寫操作修改了內容,引發數據髒讀問題
總結起來就是,允許多個線程同時讀,但只要有一個線程在寫,其他線程就必須排隊等待。
在 JDK 中有一個讀寫鎖ReadWriteLock
,使用它就可以解決這個問題,它可以保證以下兩點:
- 1.只允許一個線程寫入,其他線程既不能寫入也不能讀取
- 2.沒有寫入時,多個線程允許同時讀,可以提高程序併發性能
實際上,讀寫鎖ReadWriteLock
裏面有兩個鎖實現,一個是讀操作相關的鎖,稱爲共享鎖,當多個線程同時操作時,不會讓多個線程進行排隊等待,大大的提升了程序併發讀的執行效率;另一個是寫操作相關的鎖,稱爲排他鎖,當多個線程同時操作時,只允許一個線程寫入,其他線程進入排隊等待;兩者進行組合操作,就可以實現上面的預期效果。
下面我們一起來看看它的基本用法!
二、ReadWriteLock 基本用法
2.1、讀和讀共享
讀和讀之間不互斥,當多個線程進行讀的時候,不會讓多個線程進行排隊等待。
我們可以看一個簡單的例子!
public class Counter {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int count;
public void read() {
// 加讀鎖
lock.readLock().lock();
try {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了讀鎖,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放讀鎖
lock.readLock().unlock();
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Counter counter = new Counter();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
counter.read();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
counter.read();
}
});
threadA.start();
threadB.start();
}
}
看一下運行結果:
2023-10-23 16:12:28:119 當前線程:Thread-0獲得了讀鎖,count:0
2023-10-23 16:12:28:119 當前線程:Thread-1獲得了讀鎖,count:0
從日誌時間上可以很清晰的看到,儘管加鎖了,並且休眠了 5 秒,但是兩個線程還是幾乎同時執行try()
方法裏面的代碼,證明了讀和讀之間是不互斥的,可以顯著提高程序的運行效率。
2.2、寫和寫之間互斥
寫和寫之間互斥,當多個線程進行寫的時候,只允許一個線程寫入,其他線程進入排隊等待。
我們可以看一個簡單的例子!
public class Counter {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int count;
public void write() {
// 加寫鎖
lock.writeLock().lock();
try {
count++;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了寫鎖,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放寫鎖
lock.writeLock().unlock();
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Counter counter = new Counter();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
counter.write();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
counter.write();
}
});
threadA.start();
threadB.start();
}
}
看一下運行結果:
2023-10-23 16:29:59:103 當前線程:Thread-0獲得了寫鎖,count:1
2023-10-23 16:30:04:108 當前線程:Thread-1獲得了寫鎖,count:2
從日誌時間上可以很清晰的看到,兩個線程進行串行執行,證明了寫和寫之間是互斥的。
2.3、讀和寫之間互斥
讀和寫之間互斥,當多個線程交替進行讀寫的時候,操作上互斥,只有一個線程能進入,其他線程進入排隊等待。
我們可以看一個簡單的例子!
public class Counter {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int count;
public void read() {
// 加讀鎖
lock.readLock().lock();
try {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了讀鎖,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放讀鎖
lock.readLock().unlock();
}
}
public void write() {
// 加寫鎖
lock.writeLock().lock();
try {
count++;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 當前線程:" + Thread.currentThread().getName() + "獲得了寫鎖,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 釋放寫鎖
lock.writeLock().unlock();
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
Counter counter = new Counter();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
counter.read();
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
counter.write();
}
});
threadA.start();
threadB.start();
}
}
看一下運行結果:
2023-10-23 16:36:08:786 當前線程:Thread-0獲得了讀鎖,count:0
2023-10-23 16:36:13:791 當前線程:Thread-1獲得了寫鎖,count:1
從日誌時間上可以很清晰的看到,兩個線程進行串行執行,證明了讀和寫之間是互斥的。
三、小結
總結下來,ReadWriteLock
有以下特點:
- 允許多個線程在沒有寫入時同時讀取,可以提高讀取效率
- 當存在寫入情況時,只允許一個線程寫入,其他線程進入排隊等待
- 適合讀多寫少的場景
對於同一個數據,有大量線程讀取,但僅有少數線程修改,使用ReadWriteLock
可以顯著的提升程序併發執行效率。
例如,一個論壇的帖子,瀏覽可以看做讀取操作,是非常頻繁的,而回復可以看做寫入操作,它是不頻繁的,這種情況就可以使用ReadWriteLock
來實現。
本文主要圍繞ReadWriteLock
的基本使用做了一次知識總結,如果有不正之處,請多多諒解,並歡迎批評指出。
四、參考
1、https://www.cnblogs.com/xrq730/p/4855631.html
2、https://www.liaoxuefeng.com/wiki/1252599548343744/1306581002092578