讀寫鎖ReentrantReadWriteLock概述
大型網站中很重要的一塊內容就是資料的讀寫,ReentrantLock雖然具有完全互斥排他的效果(即同一時間只有一個執行緒正在執行lock後面的任務),但是效率非常低。所以在JDK中提供了一種讀寫鎖ReentrantReadWriteLock,使用它可以加快執行效率。
讀寫鎖表示兩個鎖,一個是讀操作相關的鎖,稱為共享鎖;另一個是寫操作相關的鎖,稱為排他鎖。我把這兩個操作理解為三句話:
1、讀和讀之間不互斥,因為讀操作不會有執行緒安全問題
2、寫和寫之間互斥,避免一個寫操作影響另外一個寫操作,引發執行緒安全問題
3、讀和寫之間互斥,避免讀操作的時候寫操作修改了內容,引發執行緒安全問題
總結起來就是,多個Thread可以同時進行讀取操作,但是同一時刻只允許一個Thread進行寫入操作。
讀和讀共享
先證明一下第一句話"讀和讀之間不互斥",舉一個簡單的例子:
public class ThreadDomain48 extends ReentrantReadWriteLock { public void read() { try { readLock().lock(); System.out.println(Thread.currentThread().getName() + "獲得了讀鎖, 時間為" + System.currentTimeMillis()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock().unlock(); } } }
public static void main(String[] args) { final ThreadDomain48 td = new ThreadDomain48(); Runnable readRunnable = new Runnable() { public void run() { td.read(); } }; Thread t0 = new Thread(readRunnable); Thread t1 = new Thread(readRunnable); t0.start(); t1.start(); }
看一下執行結果:
Thread-0獲得了讀鎖, 時間為1444019668424 Thread-1獲得了讀鎖, 時間為1444019668424
儘管方法加了鎖,還休眠了10秒,但是兩個執行緒還是幾乎同時執行lock()方法後面的程式碼,看時間就知道了。說明lock.readLock()讀鎖可以提高程式執行效率,允許多個執行緒同時執行lock()方法後面的程式碼
寫和寫互斥
再證明一下第二句話"寫和寫之間互斥",類似的證明方法:
public class ThreadDomain48 extends ReentrantReadWriteLock { public void write() { try { writeLock().lock(); System.out.println(Thread.currentThread().getName() + "獲得了寫鎖, 時間為" + System.currentTimeMillis()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock().unlock(); } } }
public static void main(String[] args) { final ThreadDomain48 td = new ThreadDomain48(); Runnable readRunnable = new Runnable() { public void run() { td.write(); } }; Thread t0 = new Thread(readRunnable); Thread t1 = new Thread(readRunnable); t0.start(); t1.start(); }
看一下執行結果:
Thread-0獲得了寫鎖, 時間為1444021393325 Thread-1獲得了寫鎖, 時間為1444021403325
從時間上就可以看出來,10000ms即10s,和程式碼裡一致,證明了讀和讀之間是互斥的
讀和寫互斥
最後證明一下第三句話"讀和寫之間互斥",證明方法無非是把上面二者結合起來而已,看一下:
public class ThreadDomain48 extends ReentrantReadWriteLock { public void write() { try { writeLock().lock(); System.out.println(Thread.currentThread().getName() + "獲得了寫鎖, 時間為" + System.currentTimeMillis()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { writeLock().unlock(); } } public void read() { try { readLock().lock(); System.out.println(Thread.currentThread().getName() + "獲得了讀鎖, 時間為" + System.currentTimeMillis()); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { readLock().unlock(); } } }
public static void main(String[] args) { final ThreadDomain48 td = new ThreadDomain48(); Runnable readRunnable = new Runnable() { public void run() { td.read(); } }; Runnable writeRunnable = new Runnable() { public void run() { td.write(); } }; Thread t0 = new Thread(readRunnable); Thread t1 = new Thread(writeRunnable); t0.start(); t1.start(); }
看一下執行結果:
Thread-0獲得了讀鎖, 時間為1444021679203 Thread-1獲得了寫鎖, 時間為1444021689204
從時間上看,也是10000ms即10s,和程式碼裡面是一致的,證明了讀和寫之間是互斥的。注意一下,"讀和寫互斥"和"寫和讀互斥"是兩種不同的場景,但是證明方式和結論是一致的,所以就不證明了。
synchronized和ReentrantLock的對比
到現在,看到多執行緒中,鎖定的方式有2種:synchronized和ReentrantLock。兩種鎖定方式各有優劣,下面簡單對比一下:
1、synchronized是關鍵字,就和if...else...一樣,是語法層面的實現,因此synchronized獲取鎖以及釋放鎖都是Java虛擬機器幫助使用者完成的;ReentrantLock是類層面的實現,因此鎖的獲取以及鎖的釋放都需要使用者自己去操作。特別再次提醒,ReentrantLock在lock()完了,一定要手動unlock()
2、synchronized簡單,簡單意味著不靈活,而ReentrantLock的鎖機制給使用者的使用提供了極大的靈活性。這點在Hashtable和ConcurrentHashMap中體現得淋漓盡致。synchronized一鎖就鎖整個Hash表,而ConcurrentHashMap則利用ReentrantLock實現了鎖分離,鎖的只是segment而不是整個Hash表
3、synchronized是不公平鎖,而ReentrantLock可以指定鎖是公平的還是非公平的
4、synchronized實現等待/通知機制通知的執行緒是隨機的,ReentrantLock實現等待/通知機制可以有選擇性地通知
5、和synchronized相比,ReentrantLock提供給使用者多種方法用於鎖資訊的獲取,比如可以知道lock是否被當前執行緒獲取、lock被同一個執行緒呼叫了幾次、lock是否被任意執行緒獲取等等
總結起來,我認為如果只需要鎖定簡單的方法、簡單的程式碼塊,那麼考慮使用synchronized,複雜的多執行緒處理場景下可以考慮使用ReentrantLock。當然這只是建議性地,還是要具體場景具體分析的。
最後,檢視了很多資料,JDK1.5版本只有由於對synchronized做了諸多優化,效率上synchronized和ReentrantLock應該是差不多。