Java 5 以後,Java在內置關鍵字sychronized的基礎上又增長了一個新的處理鎖的方式,Lock類。html
因爲在Java線程間通訊:volatile與sychronized中,咱們已經詳細的瞭解了synchronized,因此咱們如今主要介紹一下Lock,以及將Lock與synchronized進行一下對比。java
synchronized修飾的代碼只有獲取鎖的線程纔可以執行,其餘線程只能等待該線程釋放鎖。一個線程釋放鎖的狀況有如下方式:編程
咱們在Java多線程的生命週期,實現與調度中談過,鎖會由於等待I/O,sleep()方法等緣由被阻塞而不釋放鎖,此時若是線程還處於用synchronized修飾的代碼區域裏,那麼其餘線程只能等待,這樣就影響了效率。所以Java提供了Lock來實現另外一個機制,即不讓線程無限期的等待下去。安全
思考一個情景,當多線程讀寫文件時,讀操做和寫操做會發生衝突,寫操做和寫操做會發生衝突,但讀操做和讀操做不會有衝突。若是使用synchronized來修飾的話,就極可能形成多個讀操做沒法同時進行的可能(若是隻用synchronized修飾寫方法,那麼可能形成讀寫衝突,若是同時修飾了讀寫方法,則會有讀讀干擾)。此時就須要用到Lock,換言之Lock比synchronized提供了更多的功能。多線程
使用Lock須要注意如下兩點:併發
Lock類自己是一個接口,其方法以下:ide
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
下面依次講解一下其中各個方法。性能
Lock lock = ...; lock.lock(); try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 }
public void method() throws InterruptedException { lock.lockInterruptibly(); try { //..... } finally { lock.unlock(); } }
Lock lock = ...; if(lock.tryLock()) { try{ //處理任務 }catch(Exception ex){ }finally{ lock.unlock(); //釋放鎖 } }else { //若是不能獲取鎖,則直接作其餘事情 }
ReentrantLock譯爲「可重入鎖」,咱們在Java多線程:synchronized的可重入性中已經明白了什麼是可重入以及理解了synchronized的可重入性。ReentrantLock是惟一實現Lock接口的類。優化
考慮到如下情景,一個僅出售雙人票的演唱會進行門票出售,有三個售票口同時進行售票,買票須要100ms時間,每張票出票須要100ms時間。該如何設計這個情景?this
package com.cielo.LockTest; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static java.lang.Thread.sleep; /** * Created by 63289 on 2017/4/10. */ class SoldTicket implements Runnable { Lock lock = new ReentrantLock();//使用可重入鎖 private volatile Integer ticket;//保證從主內存獲取 SoldTicket(Integer ticket) { this.ticket = ticket;//提供票數 } private void sold() { lock.lock();//鎖定操做放在try代碼塊外 try { if (ticket <= 0) return;//當ticket==2時可能有多個線程進入sold方法,一個線程運行後另外兩個線程須要退出。 sleep(200);//買票0.1s,出票0.1s --ticket; System.out.println("The first ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave.");//獲取線程id來識別出票站。 sleep(100);//出票0.1s --ticket; System.out.println("The second ticket is sold by "+Thread.currentThread().getId()+", "+ticket+" tickets leave."); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } @Override public void run() { while (ticket > 0) { sold(); } } } public class LockTest { public static void main(String[] args) { SoldTicket soldTicket = new SoldTicket(20); new Thread(soldTicket).start(); new Thread(soldTicket).start(); new Thread(soldTicket).start(); } }
上面這段代碼結果以下:
The first ticket is sold by 11, 19 tickets leave. The second ticket is sold by 11, 18 tickets leave. The first ticket is sold by 13, 17 tickets leave. The second ticket is sold by 13, 16 tickets leave. The first ticket is sold by 13, 15 tickets leave. The second ticket is sold by 13, 14 tickets leave. The first ticket is sold by 12, 13 tickets leave. The second ticket is sold by 12, 12 tickets leave. The first ticket is sold by 11, 11 tickets leave. The second ticket is sold by 11, 10 tickets leave. The first ticket is sold by 11, 9 tickets leave. The second ticket is sold by 11, 8 tickets leave. The first ticket is sold by 13, 7 tickets leave. The second ticket is sold by 13, 6 tickets leave. The first ticket is sold by 13, 5 tickets leave. The second ticket is sold by 13, 4 tickets leave. The first ticket is sold by 13, 3 tickets leave. The second ticket is sold by 13, 2 tickets leave. The first ticket is sold by 13, 1 tickets leave. The second ticket is sold by 13, 0 tickets leave.
若是咱們不對售票操做進行鎖定,則會有如下幾個問題:
顯然,本題的情景用synchronized也能夠很容易的實現,實際上Lock有別於synchronized的主要點是lockInterruptibly()和tryLock()這兩個能夠對鎖進行控制的方法。
回到開頭synchronized缺陷的介紹,實際上,Lock接口的重要衍生接口ReadWriteLock便是解決這一問題。ReadWriteLock定義很簡單,僅有兩個接口:
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading. */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing. */ Lock writeLock(); }
便是它只提供了readLock()和writeLock()兩個操做,這兩個操做均返回一個Lock類的實例。兩個操做一個獲取讀鎖,一個獲取寫鎖,將讀寫分開進行操做。ReadWriteLock將讀寫的鎖分開,可讓多個讀操做並行,這就大大提升了效率。使用ReadWriteLock時,用讀鎖去控制讀操做,寫鎖控制寫操做,進而實現了一個能夠在以下的大量讀少許寫且讀者優先的情景運行的鎖。
ReentrantReadWriteLock是ReadWriteLock的惟一實例。同時提供了不少操做方法。ReentratReadWriteLock接口實現的讀鎖寫鎖進入有以下要求:
private SomeClass someClass;//資源 private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//建立鎖 private final Lock readLock = readWriteLock.readLock();//讀鎖 private final Lock writeLock = readWriteLock.writeLock();//寫鎖 //讀方法 readLock.lock(); try { result = someClass.someMethod(); } catch (Exception e) { e.printStackTrace(); } finally { readLock.unlock(); } //寫方法,產生新的SomeClass實例tempSomeClass writeLock.lock(); try{ this.someClass = tempSomeClass;//更新 }catch (Exception e) { e.printStackTrace(); } finally{ writeLock.unlock(); }
公平鎖即當多個線程等待的一個資源的鎖釋放時,線程不是隨機的獲取資源而是等待時間最久的線程獲取資源(FIFO)。Java中,synchronized是一個非公平鎖,沒法保證鎖的獲取順序。ReentrantLock和ReentrantReadWriteLock默認也是非公平鎖,但能夠設置成公平鎖。咱們前面的實例中初始化ReentrantLock和ReentrantReadWriteLock時都是無參數的。實際上,它們提供一個默認的boolean變量fair,爲true則爲公平鎖,爲false則爲非公平鎖,默認爲false。所以,當咱們想將其實現爲公平鎖時,僅須要初始化時賦值true。即:
Lock lock = new ReentrantLock(true);
考慮前面賣票的實例,若是改成公平鎖(儘管這和情景無關),則結果輸出很是整齊以下:
The first ticket is sold by 11, 19 tickets leave. The second ticket is sold by 11, 18 tickets leave. The first ticket is sold by 12, 17 tickets leave. The second ticket is sold by 12, 16 tickets leave. The first ticket is sold by 13, 15 tickets leave. The second ticket is sold by 13, 14 tickets leave. The first ticket is sold by 11, 13 tickets leave. The second ticket is sold by 11, 12 tickets leave. The first ticket is sold by 12, 11 tickets leave. The second ticket is sold by 12, 10 tickets leave. The first ticket is sold by 13, 9 tickets leave. The second ticket is sold by 13, 8 tickets leave. The first ticket is sold by 11, 7 tickets leave. The second ticket is sold by 11, 6 tickets leave. The first ticket is sold by 12, 5 tickets leave. The second ticket is sold by 12, 4 tickets leave. The first ticket is sold by 13, 3 tickets leave. The second ticket is sold by 13, 2 tickets leave. The first ticket is sold by 11, 1 tickets leave. The second ticket is sold by 11, 0 tickets leave.