java併發程式設計系列之ReadWriteLock讀寫鎖的使用
前面我們講解了Lock的使用,下面我們來講解一下ReadWriteLock鎖的使用,顧明思義,讀寫鎖在讀的時候,上讀鎖,在寫的時候,上寫鎖,這樣就很巧妙的解決synchronized的一個性能問題:讀與讀之間互斥。
ReadWriteLock也是一個介面,原型如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
該介面只有兩個方法,讀鎖和寫鎖。也就是說,我們在寫檔案的時候,可以將讀和寫分開,分成2個鎖來分配給執行緒,從而可以做到讀和讀互不影響,讀和寫互斥,寫和寫互斥,提高讀寫檔案的效率。該介面也有一個實現類ReentrantReadWriteLock,下面我們就來學習下這個類。
我們先看一下,多執行緒同時讀取檔案時,用synchronized實現的效果,程式碼如下:
public class ReadAndWriteLock { public synchronized 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) { final ReadAndWriteLock lock = new ReadAndWriteLock(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); new Thread(new Runnable() { @Override public void run() { lock.get(Thread.currentThread()); } }).start(); } }
測試結果如下:
start time:1442459467623 Thread-0:正在進行讀操作…… Thread-0:正在進行讀操作…… Thread-0:正在進行讀操作…… Thread-0:正在進行讀操作…… Thread-0:正在進行讀操作…… Thread-0:讀操作完畢! end time:1442459467723 start time:1442459467723 Thread-1:正在進行讀操作…… Thread-1:正在進行讀操作…… Thread-1:正在進行讀操作…… Thread-1:正在進行讀操作…… Thread-1:正在進行讀操作…… Thread-1:讀操作完畢! end time:1442459467823
整個過程耗時200ms
我們可以看到,及時是在讀取檔案,在加了synchronized關鍵字之後,讀與讀之間,也是互斥的,也就是說,必須等待Thread-0讀完之後,才會輪到Thread-1執行緒讀,而無法做到同時讀檔案,這種情況在大量執行緒同時都需要讀檔案的時候,讀寫鎖的效率,明顯要高於synchronized關鍵字的實現。下面我們來測試一下,程式碼如下:
public class ReadAndWriteLock {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void get(Thread thread) {
lock.readLock().lock();
try{
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());
}finally{
lock.readLock().unlock();
}
}
public static void main(String[] args) {
final ReadAndWriteLock lock = new ReadAndWriteLock();
new Thread(new Runnable() {
@Override
public void run() {
lock.get(Thread.currentThread());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
lock.get(Thread.currentThread());
}
}).start();
}
}
測試結果如下:
start time:1442460030593
start time:1442460030593
Thread-1:正在進行讀操作……
Thread-0:正在進行讀操作……
Thread-1:正在進行讀操作……
Thread-0:正在進行讀操作……
Thread-1:正在進行讀操作……
Thread-0:正在進行讀操作……
Thread-1:正在進行讀操作……
Thread-0:正在進行讀操作……
Thread-1:正在進行讀操作……
Thread-0:正在進行讀操作……
Thread-0:讀操作完畢!
Thread-1:讀操作完畢!
end time:1442460030693
end time:1442460030693
整個過程耗時:100ms
從測試結果來看,Thread-0和Thread-1是在同時讀取檔案。
通過兩次實驗的對比,我們可以看出來,讀寫鎖的效率明顯高於synchronized關鍵字
不過要注意的是,如果有一個執行緒已經佔用了讀鎖,則此時其他執行緒如果要申請寫鎖,則申請寫鎖的執行緒會一直等待釋放讀鎖。如果有一個執行緒已經佔用了寫鎖,則此時其他執行緒如果申請寫鎖或者讀鎖,則申請的執行緒會一直等待釋放寫鎖。讀鎖和寫鎖是互斥的。
下面我們來驗證下讀寫鎖的互斥關係,程式碼如下:
public class ReadAndWriteLock {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
final ReadAndWriteLock lock = new ReadAndWriteLock();
// 建N個執行緒,同時讀
ExecutorService service = Executors.newCachedThreadPool();
service.execute(new Runnable() {
@Override
public void run() {
lock.readFile(Thread.currentThread());
}
});
// 建N個執行緒,同時寫
ExecutorService service1 = Executors.newCachedThreadPool();
service1.execute(new Runnable() {
@Override
public void run() {
lock.writeFile(Thread.currentThread());
}
});
}
// 讀操作
public 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 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();
}
}
}
測試結果如下:
// 讀鎖和讀鎖測試結果:
當前為讀鎖!
當前為讀鎖!
pool-2-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-2-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-2-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-2-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-2-thread-1:正在進行讀操作……
pool-1-thread-1:讀操作完畢!
pool-2-thread-1:讀操作完畢!
釋放讀鎖!
釋放讀鎖!
// 測試結果不互斥
// 讀鎖和寫鎖,測試結果如下:
當前為讀鎖!
pool-1-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-1-thread-1:正在進行讀操作……
pool-1-thread-1:讀操作完畢!
釋放讀鎖!
當前為寫鎖!
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:寫操作完畢!
釋放寫鎖!
// 測試結果互斥
// 寫鎖和寫鎖,測試結果如下:
當前為寫鎖!
pool-1-thread-1:正在進行寫操作……
pool-1-thread-1:正在進行寫操作……
pool-1-thread-1:正在進行寫操作……
pool-1-thread-1:正在進行寫操作……
pool-1-thread-1:正在進行寫操作……
pool-1-thread-1:寫操作完畢!
釋放寫鎖!
當前為寫鎖!
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:正在進行寫操作……
pool-2-thread-1:寫操作完畢!
釋放寫鎖!
// 測試結果互斥
關於鎖的內容我們就講到這裡,下面對鎖的相關概念做一個介紹:
1.可重入(Reentrant)鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和 ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。舉個簡單的例子,當一 個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法 method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。
看下面這段程式碼就明白了:
classMyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述程式碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,執行緒A執行到了method1,此時執行緒 A獲取了這個物件的鎖,而由於method2也是synchronized方法,假如synchronized不具備可重入性,此時執行緒A需要重新申請 鎖。但是這就會造成一個問題,因為執行緒A已經持有了該物件的鎖,而又在申請獲取該物件的鎖,這樣就會執行緒A一直等待永遠不會獲取到的鎖。 而由於synchronized和Lock都具備可重入性,所以不會發生上述現象。
2.可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一執行緒A正在執行鎖中的程式碼,另一執行緒B正在等待獲取該鎖,可能由於等待時間過長,執行緒B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。
3.公平鎖
公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。
而對於ReentrantLock和ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。設定方法如下:ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
4、讀寫鎖
前面已經介紹,這裡不做贅述