1. 程式人生 > >Java中的鎖——lock介面解析

Java中的鎖——lock介面解析

提到java中的鎖,相信大家都知道它是用來控制多個執行緒訪問共享資源的方式(即鎖能防止多個執行緒同時訪問空享資源而出現執行緒安全問題)。在實踐過程中使用最多的也最常見的鎖就是 synchronized 在jdk1.5之前也僅僅有這一種鎖而已。在jdk1.5之後,併發包中新增了Lock介面(以及相關實現類)用來實現鎖功能,Lock介面提供了與synchronized關鍵字類似的同步功能,但需要在使用時手動獲取鎖和釋放鎖。雖然Lock介面沒有synchronized關鍵字自動獲取和釋放鎖那麼便捷,但Lock介面卻具有了鎖的可操作性,可中斷獲取以及超時獲取鎖等多種非常實用的同步特性,除此之外Lock介面還有兩個非常強大的實現類重入鎖和讀寫鎖

,下面一一講解這些內容。

Lock介面的使用

Lock lock  = new ReentrantLock();
lock.lock();
try{
//可能會出現執行緒安全的操作
}finally{
//一定在finally中釋放鎖
//也不能把獲取鎖在try中進行,因為有可能在獲取鎖的時候丟擲異常
  lock.ublock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Lock介面與synchronized關鍵字的區別

  • Lock介面可以嘗試非阻塞地獲取鎖 當前執行緒嘗試獲取鎖。如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖。
    *Lock介面能被中斷地獲取鎖 與synchronized不同,獲取到鎖的執行緒能夠響應中斷,當獲取到的鎖的執行緒被中斷時,中斷異常將會被丟擲,同時鎖會被釋放。
  • Lock介面在指定的截止時間之前獲取鎖,如果截止時間到了依舊無法獲取鎖,則返回。

Lock介面的API

  • void lock() 獲取鎖,呼叫該方法當前執行緒將會獲取鎖,當鎖獲取後,該方法將返回。
  • void lockInterruptibly() throws InterruptedException 可中斷獲取鎖,與lock()方法不同之處在於該方法會響應中斷,即在鎖的獲取過程中可以中斷當前執行緒
  • boolean tryLock() 嘗試非阻塞的獲取鎖,呼叫該方法立即返回,true表示獲取到鎖
  • boolean tryLock(long time,TimeUnit unit) throws InterruptedException 超時獲取鎖,以下情況會返回:時間內獲取到了鎖,時間內被中斷,時間到了沒有獲取到鎖。
  • void unlock() 釋放鎖

瞭解Lock介面最常用的實現ReentrantLock重入鎖

ReentrantLock是Lock介面一種常見的實現,它是支援重進入的鎖即表示該鎖能夠支援一個執行緒對資源的重複加鎖。該鎖還支援獲取鎖時的公平與非公平的選擇。
關於鎖的重進入,其實synchronized關鍵字也支援。如前所述,synchronized關鍵字也是隱式的支援重進入而對於ReentrantLock而言,對於已經獲取到鎖的執行緒,再次呼叫lock()方法時依然可以獲取鎖而不被阻塞。
理解了鎖的重進入,現在解釋剛剛提到的公平獲取鎖與非公平獲取鎖。如果在絕對時間上,先對於鎖進行獲取的請求一定先被滿足,那麼這個鎖就是公平的,反之就是非公平的。公平的獲取鎖也就是等待時間最久的執行緒優先獲取到鎖。ReentrantLock的建構函式來控制是否為公平鎖。

我在第一次瞭解到公平鎖於非公平鎖的時候,第一反應是公平鎖的效率高,應該使用公平鎖。但實際的情況是,非公平的鎖的效率遠遠大於公平鎖。

瞭解Lock介面的實現類ReentrantReadWriteLock讀寫鎖

前面提到的ReentrantLock是排他鎖,該鎖在同一時刻只允許一個執行緒來訪問,而讀寫鎖在同一時刻允許可以有多個執行緒來訪問,但在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過讀寫鎖分離,使得併發性相比一般的排他鎖有了很大的提升。

讀寫鎖除了使用在寫操作happends-before與讀操作以及併發性的提升之外,讀寫鎖也能夠簡化讀寫互動場景的程式設計方式。假設在程式中定義一個共享的用作快取資料結構,它的大部分時間提供讀服務(查詢,搜尋等)而寫操作較少,但寫操作之後需要立即對後續的讀操作可見。在沒有讀寫鎖之前,實現這個功能需要使用等待通知機制(http://blog.csdn.net/canot/article/details/50879963)。無論使用那種方式,目的都是為了寫操作立即可見於讀操作而避免髒讀。但使用讀寫鎖卻比等待通知簡單明瞭多了。
一般情況下,讀寫鎖效能優於排他鎖。它能提供更好的併發性和吞吐量。

ReentrantReadWriteLock讀寫鎖的幾個特性:

  • 公平選擇性
  • 重進入
  • 鎖降級

讀寫鎖的示例:快取

public class Cache{
  static Map<String,Object> map = new HashMap<String,Object>();
  static ReentrantReadWriteLock  rwl = new ReentrantReadWriteLock();
  static Lock rLock = rwl.readLock();
  static Lock wLock = rwl.writeLock();
  //獲取一個key對應的value
  public static final Object get(String key){
  r.lock();
  try{
   return map.get(key);
   }finally{
    r.unlock();
    }
  }
  //設定key對應的value並返回舊的value
  public static fianl Object put(String key,Object value){
  w.lock();
  try{
   return map.put(key,value);
   }final{
   w.unlock();
    }
  }
  //清空快取
  public static fianl void clear(){
  w.lock();
  try{
     map.clear();
   } finally{
    w.unlock();
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

上述快取示例中,我們使用了一個非執行緒安全的HashMap作為快取的時候然後使用讀寫鎖來保證執行緒安全。Cache使用讀寫鎖提升讀操作的併發性,也保證每次寫操作對讀操作的及時可見性,同時簡化了程式設計方式。

讀寫鎖的鎖降級

鎖降級是指寫鎖降級成為讀鎖。如果當前執行緒持有寫鎖,然後將其釋放再獲取讀鎖的過程不能稱為鎖降級。鎖降級指的在持有寫鎖的時候再獲取讀鎖,獲取到讀鎖後釋放之前寫鎖的過程稱為鎖釋放。

鎖降級在某些情況下是非常必要的,主要是為了保證資料的可見性。如果當前執行緒不獲取讀鎖而直接釋放寫鎖,假設此時另外一個執行緒獲取了寫鎖並修改了資料。那麼當前執行緒無法感知該執行緒的資料更新。