1. 程式人生 > >java併發程式設計系列之ReadWriteLock讀寫鎖的使用

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、讀寫鎖

前面已經介紹,這裡不做贅述