1. 程式人生 > >JAVA鎖機制-可重入鎖,可中斷鎖,公平鎖,讀寫鎖,自旋鎖

JAVA鎖機制-可重入鎖,可中斷鎖,公平鎖,讀寫鎖,自旋鎖

部落格引用處(以下內容在原有部落格基礎上進行補充或更改,謝謝這些大牛的部落格指導):
JAVA鎖機制-可重入鎖,可中斷鎖,公平鎖,讀寫鎖,自旋鎖
在這裡插入圖片描述

在併發程式設計中,經常遇到多個執行緒訪問同一個 共享資源 ,這時候作為開發者必須考慮如何維護資料一致性,在java中synchronized關鍵字被常用於維護資料一致性。synchronized機制是給共享資源上鎖,只有拿到鎖的執行緒才可以訪問共享資源,這樣就可以強制使得對共享資源的訪問都是順序的,因為對於共享資源屬性訪問是必要也是必須的,下文會有具體示例演示。
一.java中的鎖
一般在java中所說的鎖就是指的內建鎖,每個java物件都可以作為一個實現同步的鎖,雖然說在java中一切皆物件, 但是鎖必須是引用型別的,基本資料型別則不可以 。每一個引用型別的物件都可以隱式的扮演一個用於同步的鎖的角色,執行執行緒進入synchronized塊之前會自動獲得鎖,無論是通過正常語句退出還是執行過程中丟擲了異常,執行緒都會在放棄對synchronized塊的控制時自動釋放鎖。 獲得鎖的唯一途徑就是進入這個內部鎖保護的同步塊或方法 。
正如引言中所說,對共享資源的訪問必須是順序的,也就是說當多個執行緒對共享資源訪問的時候,只能有一個執行緒可以獲得該共享資源的鎖,當執行緒A嘗試獲取執行緒B的鎖時,執行緒A必須等待或者阻塞,直到執行緒B釋放該鎖為止,否則執行緒A將一直等待下去,因此java內建鎖也稱作互斥鎖,也即是說鎖實際上是一種互斥機制。
根據使用方式的不同一般我們會將鎖分為物件鎖和類鎖,兩個鎖是有很大差別的,物件鎖是作用在例項方法或者一個物件例項上面的,而類鎖是作用在靜態方法或者Class物件上面的。一個類可以有多個例項物件,因此一個類的物件鎖可能會有多個,但是每個類只有一個Class物件,所以類鎖只有一個。 類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定的是例項方法還是靜態方法區別的 。
在java中實現鎖機制不僅僅限於使用synchronized關鍵字,還有JDK1.5之後提供的Lock,Lock不在本文討論範圍之內。一個synchronized塊包含兩個部分:鎖物件的引用,以及這個鎖保護的程式碼塊。如果作用在例項方法上面,鎖就是該方法所在的當前物件,靜態synchronized方法會從Class物件上獲得鎖。

鎖的相關概念介紹

1.可重入鎖

如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明瞭鎖的分配機制:基於執行緒的分配,而不是基於方法呼叫的分配。舉個簡單的例子,當一個執行緒執行到某個synchronized方法時,比如說method1,而在method1中會呼叫另外一個synchronized方法method2,此時執行緒不必重新去申請鎖,而是可以直接執行方法method2。

看下面這段程式碼就明白了:

class MyClass {
    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不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的執行緒中中斷它,這種就是可中斷鎖。

在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。

3.公平鎖

公平鎖即儘量以請求鎖的順序來獲取鎖。比如同是有多個執行緒在等待一個鎖,當這個鎖被釋放時,等待時間最久的執行緒(最先請求的執行緒)會獲得該所,這種就是公平鎖。

非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些執行緒永遠獲取不到鎖。

在Java中,synchronized就是非公平鎖,它無法保證等待的執行緒獲取鎖的順序。

而對於ReentrantLock和ReentrantReadWriteLock,它預設情況下是非公平鎖,但是可以設定為公平鎖。

看一下這2個類的原始碼就清楚了:
在這裡插入圖片描述

在ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。

我們可以在建立ReentrantLock物件時,通過以下方式來設定鎖的公平性:

ReentrantLock lock = new ReentrantLock(true);

如果引數為true表示為公平鎖,為fasle為非公平鎖。預設情況下,如果使用無參構造器,則是非公平鎖。

在這裡插入圖片描述

另外在ReentrantLock類中定義了很多方法,比如:

isFair()        //判斷鎖是否是公平鎖

isLocked()    //判斷鎖是否被任何執行緒獲取了

isHeldByCurrentThread()   //判斷鎖是否被當前執行緒獲取了

hasQueuedThreads()   //判斷是否有執行緒在等待該鎖

在ReentrantReadWriteLock中也有類似的方法,同樣也可以設定為公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock並未實現Lock介面,它實現的是ReadWriteLock介面。

4.讀寫鎖

讀寫鎖將對一個資源(比如檔案)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。

正因為有了讀寫鎖,才使得多個執行緒之間的讀操作不會發生衝突。

ReadWriteLock就是讀寫鎖,它是一個介面,ReentrantReadWriteLock實現了這個介面。

可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。

5、自旋鎖
首先是一種鎖,與互斥鎖相似,基本作用是用於執行緒(程序)之間的同步。與普通鎖不同的是,一個執行緒A在獲得普通鎖後,如果再有執行緒B試圖獲取鎖,那麼這個執行緒B將會掛起(阻塞);試想下,如果兩個執行緒資源競爭不是特別激烈,而處理器阻塞一個執行緒引起的執行緒上下文的切換的代價高於等待資源的代價的時候(鎖的已保持者保持鎖時間比較短),那麼執行緒B可以不放棄CPU時間片,而是在“原地”忙等,直到鎖的持有者釋放了該鎖,這就是自旋鎖的原理,可見自旋鎖是一種非阻塞鎖。

二、自旋鎖可能引起的問題:
1.過多佔據CPU時間:如果鎖的當前持有者長時間不釋放該鎖,那麼等待者將長時間的佔據cpu時間片,導致CPU資源的浪費,因此可以設定一個時間,當鎖持有者超過這個時間不釋放鎖時,等待者會放棄CPU時間片阻塞;
2.死鎖問題:試想一下,有一個執行緒連續兩次試圖獲得自旋鎖(比如在遞迴程式中),第一次這個執行緒獲得了該鎖,當第二次試圖加鎖的時候,檢測到鎖已被佔用(其實是被自己佔用),那麼這時,執行緒會一直等待自己釋放該鎖,而不能繼續執行,這樣就引起了死鎖。因此遞迴程式使用自旋鎖應該遵循以下原則:遞迴程式決不能在持有自旋鎖時呼叫它自己,也決不能在遞迴呼叫時試圖獲得相同的自旋鎖。

JAVA中一種自旋鎖的實現: CAS是Compare And Set的縮寫

import java.util.concurrent.atomic.AtomicReference;  
class SpinLock {  
        //java中原子(CAS)操作  
    AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的執行緒物件  
    private int count;  
    public void lock() {  
        Thread cur = Thread.currentThread();  
        //lock函式將owner設定為當前執行緒,並且預測原來的值為空。unlock函式將owner設定為null,並且預測值為當前執行緒。當有第二個執行緒呼叫lock操作時由於owner值不為空,導致迴圈    
  
            //一直被執行,直至第一個執行緒呼叫unlock函式將owner設定為null,第二個執行緒才能進入臨界區。  
        while (!owner.compareAndSet(null, cur)){  
        }  
    }  
    public void unLock() {  
        Thread cur = Thread.currentThread();  
            owner.compareAndSet(cur, null);  
        }  
    }  
}  
public class Test implements Runnable {  
    static int sum;  
    private SpinLock lock;  
      
    public Test(SpinLock lock) {  
        this.lock = lock;  
    }  
    public static void main(String[] args) throws InterruptedException {  
        SpinLock lock = new SpinLock();  
        for (int i = 0; i < 100; i++) {  
            Test test = new Test(lock);  
            Thread t = new Thread(test);  
            t.start();  
        }  
          
        Thread.currentThread().sleep(1000);  
        System.out.println(sum);  
    }  
      
    @Override  
    public void run() {  
        this.lock.lock();  
        sum++;  
        this.lock.unLock();  
    }  
}

二.synchronized使用示例
1.多視窗售票
假設一個火車票售票系統,有若干個視窗同時售票,很顯然在這裡票是作為多個視窗的共享資源存在的,由於座位號是確定的,因此票上面的號碼也是確定的,我們用多個執行緒來模擬多個視窗同時售票,首先在不使用synchronized關鍵字的情況下測試一下售票情況。
先將票本身作為一個共享資源放在單獨的執行緒中,這種作為共享資源存在的執行緒很顯然應該是實現Runnable介面,我們將票的總數num作為一個入參傳入,每次生成一個票之後將num做減法運算,直至num為0即停止,說明票已經售完了,然後開啟多個執行緒將票資源傳入。

public class Ticket implements Runnable{
     private int num;//票數量
     private boolean flag=true;//若為false則售票停止
     public Ticket(int num){
     this.num=num;
     }
     @Override
     public void run() {
     while(flag){
     ticket();
     }
     }
     private void ticket(){
     if(num<=0){
     flag=false;
     return;
     }
     try {
     Thread.sleep(20);//模擬延時操作
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //輸出當前視窗號以及出票序列號
     System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--);
     }
    }
    public class MainTest {
     public static void main(String[] args) {
     Ticketticket = new Ticket(5);
     Threadwindow01 = new Thread(ticket, "視窗01");
     Threadwindow02 = new Thread(ticket, "視窗02");
     Threadwindow03 = new Thread(ticket, "視窗03");
     window01.start();
     window02.start();
     window03.start();
     }
    }

程式的輸出結果如下:

    視窗02售出票序列號:5
    視窗03售出票序列號:4
    視窗01售出票序列號:5
    視窗02售出票序列號:3
    視窗01售出票序列號:2
    視窗03售出票序列號:2
    視窗02售出票序列號:1
    視窗03售出票序列號:0
    視窗01售出票序列號:-1

從上面程式執行結果可以看出不但票的序號有重號而且出票數量也不對,這種售票系統比12306可要爛多了,人家在繁忙的時候只是刷不到票而已,而這裡的售票系統倒好了,出票比預計的多了而且會出現多個人爭搶做同一個座位的風險。如果是單個售票視窗是不會出現這種問題,多視窗同時售票就會出現爭搶共享資源因此紊亂的現象,解決該現象也很簡單,就是在ticket()方法前面加上synchronized關鍵字或者將ticket()方法的方法體完全用synchronized塊包括起來。

//方式一
    private synchronized void ticket(){
     if(num<=0){
     flag=false;
     return;
     }
     try {
     Thread.sleep(20);//模擬延時操作
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--);
    }
    //方式二
    private void ticket(){
     synchronized (this) {
     if (num <= 0) {
     flag = false;
     return;
     }
     try {
     Thread.sleep(20);//模擬延時操作
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName() + "售出票序列號:" + num--);
     }
    }

再看一下加入synchronized關鍵字的程式執行結果:

    視窗01售出票序列號:5
    視窗03售出票序列號:4
    視窗03售出票序列號:3
    視窗02售出票序列號:2
    視窗02售出票序列號:1

從這裡可以看出在例項方法上面加上synchronized關鍵字的實現效果跟對整個方法體加上synchronized效果是一樣的。 另外一點需要注意加鎖的時機也非常重要 ,本示例中ticket()方法中有兩處操作容易出現紊亂,一個是在if語句模組,一處是在num–,這兩處操作本身都不是原子型別的操作,但是在使用執行的時候需要這兩處當成一個整體操作,所以synchronized將整個方法體都包裹在了一起。如若不然,假設num當前值是1,但是視窗01執行到了num–,整個操作還沒執行完成,只進行了賦值運算還沒進行自減運算,但是視窗02已經進入到了if語句模組,此時num還是等於1,等到視窗02執行到了輸出語句的時候,視窗01的num–也已經將自減運算執行完成,這時候視窗02就會輸出序列號0的票。再者如果將synchronized關鍵字加在了run方法上面,這時候的操作不會出現紊亂或者錯誤,但是這種加鎖方式無異於單視窗操作,當視窗01拿到鎖進入run()方法之後,必須等到flag為false才會將語句執行完成跳出迴圈,這時候的num就已經為0了,也就是說票已經被售賣完了,這種方式摒棄了多執行緒操作,違背了最初的設計原則-多視窗售票。

**2.懶漢式單例模式**
建立單例模式有很多中實現方式,本文只討論懶漢式建立。在Android開發過程中單例模式可以說是最常使用的一種設計模式,因為它操作簡單還可以有效減少記憶體溢位。下面是懶漢式建立單例模式一個示例:

(懶漢式與餓漢式的區別:Singleton 單例模式(懶漢方式和餓漢方式)

public class Singleton {
     private static Singletoninstance;
     private Singleton() {
     }
     public static SingletongetInstance() {
     if (instance == null) {
     instance = new Singleton();
     }
     return instance;
     }
    }

如果對於多視窗售票邏輯已經完全明白了的話就可以看出這裡的實現方式是有問題的,我們可以簡單的建立幾個執行緒來獲取單例輸出物件的hascode值。

    [email protected]
    [email protected]
    [email protected]

在多執行緒模式下發現會出現不同的物件,這種單例模式很顯然不是我們想要的,那麼根據上面多視窗售票的邏輯我們在getInstance()方法上面加上一個synchronized關鍵字,給該方法加上鎖,加上鎖之後可以避免多執行緒模式下生成多個不同物件,但是同樣會帶來一個效率問題,因為不管哪個線性進入getInstance()方法都會先獲得鎖,然後再次釋放鎖,這是一個方面,另一個方面就是隻有在第一次呼叫getInstance()方法的時候,也就是在if語句塊內才會出現多執行緒併發問題,而我們卻索性將整個方法都上鎖了。討論到這裡就引出了另外一個問題,究竟是synchronized方法好還是synchronized程式碼塊好呢? 有一個原則就是鎖的範圍越小越好 ,加鎖的目的就是將鎖進去的程式碼作為原子性操作,因為非原子操作都不是執行緒安全的,因此synchronized程式碼塊應該是在開發過程中優先考慮使用的加鎖方式。

public static SingletongetInstance() {
     if (instance == null) {
     synchronized (Singleton.class) {
     instance = new Singleton();
     }
     }
     return instance;
    }

這裡也會遇到類似上面的問題,多執行緒併發下回生成多個例項,如執行緒A和執行緒B都進入if語句塊,假設執行緒A先獲得鎖,執行緒B則等待,當new一個例項後,執行緒A釋放鎖,執行緒B獲得鎖後會再次執行new語句,同樣不能保證單例要求,那麼下面程式碼再來一個null判斷,進行雙重檢查上鎖呢?

public static SingletongetInstance() {
     if (instance == null) {
     synchronized (Singleton.class) {
     if(instance==null){
     instance = new Singleton();
     }
     }
     }
     return instance;
    }

該模式就是雙重檢查上鎖實現的單例模式,這裡在程式碼層面我們已經 基本 保證了執行緒安全了,但是還是有問題的, 雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利執行。雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現bug,而是歸咎於java平臺記憶體模型。記憶體模型允許所謂的“無序寫入”,這也是這些習語失敗的一個主要原因。 更為詳細的介紹可以參考 Java單例模式中雙重檢查鎖的問題 。所以單例模式建立比較建議使用惡漢式建立或者靜態內部類方式建立。

3.synchronized不具有繼承性
我們可以通過一個簡單的demo驗證這個問題,在一個方法中順序的輸出一系列數字,並且輸出該數字所在的執行緒名稱,在父類中加上synchronized關鍵字,子類重寫父類方法測試一下加上synchronized關鍵字和不加關鍵字的區別即可

public class Parent {
     public synchronized void test() {
     for (int i = 0; i < 5; i++) {
     System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     }
     }
    }

子類繼承父類Parent,重寫test()方法.

public class Child extends Parent {
     @Override
     public void test() {
     for (int i = 0; i < 5; i++) {
     System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     }
     }
    }

測試程式碼如下:

final Child c = new Child();
    new Thread() {
     public void run() {
     c.test();
     };
    }.start();
    new Thread() {
     public void run() {
     c.test();
     };
    }.start();

輸出結果如下:

    Parent Thread-0:0  Child Thread-0:0
    Parent Thread-0:1  Child Thread-1:0
    Parent Thread-0:2  Child Thread-0:1
    Parent Thread-0:3  Child Thread-1:1
    Parent Thread-0:4  Child Thread-0:2
    Parent Thread-1:0  Child Thread-1:2
    Parent Thread-1:1  Child Thread-0:3
    Parent Thread-1:2  Child Thread-1:3
    Parent Thread-1:3  Child Thread-0:4
    Parent Thread-1:4  Child Thread-1:4

通過輸出資訊可以知道,父類Parent中會將單個執行緒中序列號輸出完成才會執行另一個執行緒中程式碼,但是子類Child中確是兩個執行緒交替輸出數字,所以synchronized不具有繼承性。

4.死鎖示例
死鎖是多執行緒開發中比較常見的一個問題。若有多個執行緒訪問多個資源時,相互之間存在競爭,就容易出現死鎖。下面就是一個死鎖的示例,當一個執行緒等待另一個執行緒持有的鎖時,而另一個執行緒也在等待該執行緒鎖持有的鎖,這時候兩個執行緒都會處於阻塞狀態,程式便出現死鎖。

package com.lock;
   class Thread01 extends Thread{
    private Object resource01;
    private Object resource02;
    public Thread01(Object resource01, Object resource02) {
    this.resource01 = resource01;
    this.resource02 = resource02;
    }
    @Override
    public void run() {
    synchronized(resource01){
    System.out.println("Thread01 locked resource01");
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource02) {
    System.out.println("Thread01 locked resource02");
    }
    }
    }
   }
    class Thread02 extends Thread{
    private Object resource01;
    private Object resource02;
    public Thread02(Object resource01, Object resource02) {
    this.resource01 = resource01;
    this.resource02 = resource02;
    
    }
    @Override
    public void run() {
    synchronized(resource02){
    System.out.println("Thread02 locked resource02");
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource01) {
    System.out.println("Thread02 locked resource01");
    }
    }
    }
   }
   public class deadlock {
    public static void main(String[] args) {
    final Object resource01="resource01";
    final Object resource02="resource02";
    Thread01 thread01=new Thread01(resource01, resource02);
    Thread02 thread02=new Thread02(resource01, resource02);
    thread01.start();
    thread02.start();
    }
   }

結果為:

Thread02 locked resource02
Thread01 locked resource01

執行上面的程式就會一直等待下去,出現死鎖。當執行緒Thread01獲得resource01的鎖後,等待500ms,然後嘗試獲取resource02的鎖,但是此時resouce02鎖已經被Thread02持有,同樣Thread02也等待了500ms嘗試獲取resouce01鎖,但是該所已經被Thread01持有,這樣兩個執行緒都在等待對方所有的資源,造成了死鎖。

三.其它
關鍵字synchronized具有鎖重入功能,當一個執行緒已經持有一個物件鎖後,再次請求該物件鎖時是可以得到該物件的鎖的,這種方式是必須的,否則在一個synchronized方法內部就沒有辦法呼叫該物件的另外一個synchronized方法了。鎖重入是通過為每個所關聯一個計數器和一個佔有它的執行緒,當計數器為0時,認為鎖是未被佔有的。執行緒請求一個未被佔有的鎖時,JVM會記錄鎖的佔有者,並將計數器設定為1。如果同一個執行緒再次請求該鎖,計數器會遞增,每次佔有的執行緒退出同步程式碼塊時計數器會遞減,直至減為0時鎖才會被釋放。
在宣告一個物件作為鎖的時候要注意字串型別鎖物件,因為字串有一個常量池,如果不同的執行緒持有的鎖是具有相同字元的字串鎖時,兩個鎖實際上同一個鎖。

ReentrantLock特性

輪詢鎖的和定時鎖

可輪詢和可定時的鎖請求是通過tryLock()方法實現的,和無條件獲取鎖不一樣. ReentrantLock可以有靈活的容錯機制.死鎖的很多情況是由於順序鎖引起的, 不同執行緒在試圖獲得鎖的時候阻塞,並且不釋放自己已經持有的鎖, 最後造成死鎖. tryLock()方法在試圖獲得鎖的時候,如果該鎖已經被其它執行緒持有,則按照設定方式立刻返回,而不是一直阻塞等下去,同時在返回後釋放自己持有的鎖.可以根據返回的結果進行重試或者取消,進而避免死鎖的發生.

公平性

ReentrantLock建構函式中提供公平性鎖和非公平鎖(預設)兩種選擇。所謂公平鎖,執行緒將按照他們發出請求的順序來獲取鎖,不允許插隊;但在非公平鎖上,則允許插隊:當一個執行緒發生獲取鎖的請求的時刻,如果這個鎖是可用的,那這個執行緒將跳過所在佇列裡等待執行緒並獲得鎖。我們一般希望所有鎖是非公平的。因為當執行加鎖操作時,公平性將講由於執行緒掛起和恢復執行緒時開銷而極大的降低效能。考慮這麼一種情況:A執行緒持有鎖,B執行緒請求這個鎖,因此B執行緒被掛起;A執行緒釋放這個鎖時,B執行緒將被喚醒,因此再次嘗試獲取鎖;與此同時,C執行緒也請求獲取這個鎖,那麼C執行緒很可能在B執行緒被完全喚醒之前獲得、使用以及釋放這個鎖。這是種雙贏的局面,B獲取鎖的時刻(B被喚醒後才能獲取鎖)並沒有推遲,C更早地獲取了鎖,並且吞吐量也獲得了提高。在大多數情況下,非公平鎖的效能要高於公平鎖的效能。

可中斷獲鎖獲取操作

lockInterruptibly方法能夠在獲取鎖的同時保持對中斷的響應,因此無需建立其它型別的不可中斷阻塞操作。

讀寫鎖ReadWriteLock

​ReentrantLock是一種標準的互斥鎖,每次最多隻有一個執行緒能持有鎖。讀寫鎖不一樣,暴露了兩個Lock物件,其中一個用於讀操作,而另外一個用於寫操作。

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();
}

可選擇實現:

1.釋放優先
2.讀執行緒插隊
3.重入性
4.降級
5.升級

ReentrantReadWriteLock實現了ReadWriteLock介面,構造器提供了公平鎖和非公平鎖兩種建立方式。讀寫鎖適用於讀多寫少的情況,可以實現更好的併發性。