基本介紹:
讀寫鎖:ReadWriteLock
在多線程的環境下,對同一份資料進行讀寫,會涉及到安全執行緒的問題。比如在一個線程讀取資料的時候,另外一個線程在寫資料,而導致前後資料的不一致性;一個線程在寫資料的時候,另一個線程也在寫,同樣也會導致線程前後看到的資料的不一致性。
這時候可以在讀寫方法中加入互斥鎖,任何時候只能允許一個線程的一個讀或寫操作,而不允許其他線程的讀或寫操作,這樣是可以解決這樣以上的問題,但是效率卻大打折扣了。因為在真實的業務情境中,一份資料,讀取資料的操作次數通常高於寫入資料的操作,而線程與線程間的讀讀操作是不涉及到安全執行緒的問題,沒有必要加入互斥鎖,只要在讀-寫,寫-寫期 間上鎖就行了。
對於這種情況,讀寫鎖則最好的解決方案。
ReentrantReadWriteLock中定義了2個內部類,ReentrantReadWriteLock.ReadLock和ReentrantReadWriteLock.WriteLock,分別用來代表讀取鎖和寫入鎖,ReentrantReadWriteLock對象提供了readLock()和writeLock()方法,用於擷取讀取鎖和寫入鎖
其中: 讀取鎖允許多個reader線程同時持有,而寫入鎖最多隻能有一個writer線程持有。 讀寫鎖的使用場合是:讀取資料的頻率遠大於修改共用資料的頻率。在上述場合下使用讀寫鎖控制共用資源的訪問,可以提高並發效能。 如果一個線程已經持有了寫入鎖,則可以再持有讀寫鎖。相反,如果一個線程已經持有了讀取鎖,則在釋放該讀取鎖之前,不能再持有寫入鎖。 可以調用寫入鎖的newCondition()方法擷取與該寫入鎖綁定的Condition對象,此時與普通的互斥鎖並沒有什麼區別,但是調用讀取鎖的newCondition()方法將拋出異常。
兩種互斥鎖機制:
1、synchronized
2、ReentrantLock
ReentrantLock是jdk5的新特性,採用ReentrantLock可以完全替代替換synchronized傳統的鎖機制,而且採用ReentrantLock的方式更加物件導向,也更加靈活
讀寫鎖的機制:
"讀-讀"不互斥
"讀-寫"互斥
"寫-寫"互斥
即在任何時候必須保證:
只有一個線程在寫入;
線程正在讀取的時候,寫入操作等待;
線程正在寫入的時候,其他線程的寫入操作和讀取操作都要等待;
鎖降級:從寫鎖變成讀鎖;鎖定擴大:從讀鎖變成寫鎖。讀鎖是可以被多線程共用的,寫鎖是單線程獨佔的。也就是說寫鎖的並發限制比讀鎖高
如下代碼會產生死結,因為同一個線程中,在沒有釋放讀鎖的情況下,就去申請寫鎖,這屬於鎖定擴大,ReentrantReadWriteLock是不支援的。
ReadWriteLock rtLock = new ReentrantReadWriteLock();rtLock.readLock().lock();System.out.println("get readLock.");rtLock.writeLock().lock();System.out.println("blocking");</span></span>
ReentrantReadWriteLock支援鎖降級,如下代碼不會產生死結。
ReadWriteLock rtLock = new ReentrantReadWriteLock();rtLock.writeLock().lock();System.out.println("writeLock");rtLock.readLock().lock();System.out.println("get read lock");
這段代碼雖然不會導致死結,但沒有正確的釋放鎖。從寫鎖降級成讀鎖,並不會自動釋放當前線程擷取的寫鎖,仍然需要顯示的釋放,否則別的線程永遠也擷取不到寫鎖。鎖的釋放和擷取可以看下:可重新進入鎖的擷取和釋放需要注意的一點兒事
<span style="white-space:pre"></span>final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();Thread wt = new Thread(new Runnable(){public void run(){readWriteLock.writeLock().lock();System.out.println("writeLock");readWriteLock.readLock().lock();System.out.println("readLock");readWriteLock.readLock().unlock();System.out.println("block");}});wt.start();try {Thread.sleep(100);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}System.out.println("main blocking.");readWriteLock.readLock().lock();</span>
應用情境:
讀寫鎖的適用情境
讀多寫少的高並發環境下,可以說這個情境算是最適合使用ReadWriteLock 了。
單利模式代碼:
import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Singleton { private static Singleton instance = null; private static ReadWriteLock rwl = new ReentrantReadWriteLock(); private Singleton(){ } /** * 當前線程在擷取到寫鎖的過程中,可以擷取到讀鎖,這叫鎖的重入,然後導致了寫鎖的降級,稱為降級鎖。 從 3 獲得寫鎖 到 5變成讀鎖 降級 利用重入可以將寫鎖降級,但只能在當前線程保持的所有寫入鎖都已經釋放後,才允許重入 reader使用它們。 6 所以在重入的過程中,其他的線程不會有擷取到鎖的機會(這樣做的好處)。試想,先釋放寫鎖,在上讀鎖,這樣做有什麼弊端。(如果5和6顛倒) 如果這樣做,那麼在釋放寫鎖後,在得到讀鎖前,有可能被其他線程打斷。 重入————>降級鎖的步驟:先擷取寫入鎖3,然後擷取讀取鎖5,最後釋放寫入鎖6(重點) * @return */ public static Singleton getInstance(){ rwl.readLock().lock(); //1 try { if (null == instance) { rwl.readLock().unlock(); //2 rwl.writeLock().lock(); //3 if (null == instance) //4 { instance = new Singleton(); } rwl.readLock().lock(); //5 rwl.writeLock().unlock(); //6 } } finally { rwl.readLock().unlock(); //7 } return instance; } }
程式碼分析:
當有n多線程 使用同Singleton 執行個體對象 調用getInstance方法時,就會產生線程的並發問題.
@1行,當有線程正在對資料進行 寫操作的時候,運行到@1行的線程要等待 寫操作的完成,因為第一個運行到@3的線程會加上鎖,然後對資料進行需該,期間不允許任何線程進行讀或者是寫的操作,
當寫完後,在該線程上加上讀鎖操作,以防止解寫鎖後,別的線程對資料再次進行寫時出錯.在第一個運行到@3的線程之後的很多線程,
可能已經運行到了@2,當對資料修改好之後,解除掉寫鎖,別的線程就會執行到@3,這時第一個線程已經經資料修改好了,所以有了@4的判斷。
在編寫多線程程式的時候,要置於並發線程的環境下考慮,巧妙的運用ReentrantReadWriteLock,在運用時,注意鎖的降級,寫入鎖可以獲得讀鎖,讀鎖不可以獲得寫入鎖,所以在上寫入鎖時,必須先將讀鎖進行解除,然後上讀鎖。
使用時注意的幾個方面:
讀鎖是排寫鎖操作的,讀鎖不排讀鎖操作,多個讀鎖可以並發不阻塞。即在讀鎖擷取後和讀鎖釋放之前,寫鎖並不能被任何線程獲得,
多個讀鎖同時作用期間,試圖擷取寫鎖的線程都處於等待狀態,當最後一個讀鎖釋放後,試圖擷取寫鎖的線程才有機會擷取寫鎖。
寫鎖是排寫鎖、排讀鎖操作的。當一個線程擷取到寫鎖之後,其他試圖擷取寫鎖和試圖擷取讀鎖的線程都處於等待狀態,直到寫鎖被釋放。
寫鎖是可以獲得讀鎖的,即:
rwl.writeLock().lock();
//在寫鎖狀態中,可以擷取讀鎖
rwl.readLock().lock();
rwl.writeLock().unlock();
讀鎖是不能夠獲得寫鎖的,如果要加寫鎖,本線程必須釋放所持有的讀鎖,即:
rwl.readLock().lock();
//......
//必須釋放掉讀鎖,才能夠加寫鎖
rwl.readLock().unlock();
rwl.writeLock().lock();
當前線程在擷取到寫鎖的過程中,可以擷取到讀鎖,這叫鎖的重入,然後導致了寫鎖的降級,稱為降級鎖。
從 3 獲得寫鎖 到 5變成讀鎖降級
利用重入可以將寫鎖降級,但只能在當前線程保持的所有寫入鎖都已經釋放後,才允許重入 reader使用它們。 6
所以在重入的過程中,其他的線程不會有擷取到鎖的機會(這樣做的好處)。試想,先釋放寫鎖,在上讀鎖,這樣做有什麼弊端。(如果5和6顛倒)
如果這樣做,那麼在釋放寫鎖後,在得到讀鎖前,有可能被其他線程打斷。
重入————>降級鎖的步驟:先擷取寫入鎖3,然後擷取讀取鎖5,最後釋放寫入鎖6(重點)