【轉】ReentrantLock--synchronized和ReentrantLock區別及使用

synchronized原語和ReentrantLock在一般情況下沒有什么區別,但是在非常復雜的同步應用中,請考慮使用ReentrantLock,特別是遇到下面2種需求的時候。
1.某個線程在等待一個鎖的控制權的這段時間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock里面的Condition應用,能夠控制notify哪個線程
3.具有公平鎖功能,每個到來的線程都將排隊等候
下面細細道來……

先說第一種情況,ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B2個線程去競爭鎖,A線程得到了鎖,B線程等待,但是A線程這個時候實在有太多事情要處理,就是一直不返回,B線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。這個時候ReentrantLock就提供了2種機制,第一,B線程中斷自己(或者別的線程中斷它),但是ReentrantLock不去響應,繼續讓B線程等待,你再怎么中斷,我全當耳邊風(synchronized原語就是如此);第二,B線程中斷自己(或者別的線程中斷它),ReentrantLock處理了這個中斷,并且不再等待這個鎖的到來,完全放棄。(如果你沒有了解java的中斷機制,請參考下相關資料,再回頭看這篇文章,80%的人根本沒有真正理解什么是java的中斷,呵呵)

這里來做個試驗,首先搞一個Buffer類,它有讀操作和寫操作,為了不讀到臟數據,寫和讀都需要加鎖,我們先用synchronized原語來加鎖,如下:

package cn.vicky.chapt10;

/**
 *
 * @author Vicky.H
 */
public class Buffer {

    private Object lock;

    public Buffer() {
        lock = this;
    }

    public void write() {
        synchronized (lock) {
            long startTime = System.currentTimeMillis();
            System.out.println("開始往這個buff寫入數據…");
            for (;;)// 模擬要處理很長時間    
            {
                if (System.currentTimeMillis()
                        - startTime > Integer.MAX_VALUE) {
                    break;
                }
            }
            System.out.println("終于寫完了");
        }
    }

    public void read() {
        synchronized (lock) {
            System.out.println("從這個buff讀數據");
        }
    }

    public static void main(String[] args) {
        Buffer buff = new Buffer();

        final Writer writer = new Writer(buff);
        final Reader reader = new Reader(buff);

        writer.start();
        reader.start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    //等5秒鐘去中斷讀    
                    if (System.currentTimeMillis()
                            - start > 5000) {
                        System.out.println("不等了,嘗試中斷");
                        reader.interrupt();
                        break;
                    }

                }

            }
        }).start();
        // 我們期待“讀”這個線程能退出等待鎖,可是事與愿違,一旦讀這個線程發現自己得不到鎖,
        // 就一直開始等待了,就算它等死,也得不到鎖,因為寫線程要21億秒才能完成 T_T ,即使我們中斷它,
        // 它都不來響應下,看來真的要等死了。這個時候,ReentrantLock給了一種機制讓我們來響應中斷,
        // 讓“讀”能伸能屈,勇敢放棄對這個鎖的等待。我們來改寫Buffer這個類,就叫BufferInterruptibly吧,可中斷緩存。
    }
}

class Writer extends Thread {

    private Buffer buff;

    public Writer(Buffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {
        buff.write();
    }
}

class Reader extends Thread {

    private Buffer buff;

    public Reader(Buffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {

        buff.read();//這里估計會一直阻塞    

        System.out.println("讀結束");

    }
}


package cn.vicky.chapt10;

import java.util.concurrent.locks.ReentrantLock;

/**
 *
 * @author Vicky.H
 */
public class BufferInterruptibly {

    private ReentrantLock lock = new ReentrantLock();

    public void write() {
        lock.lock();
        try {
            long startTime = System.currentTimeMillis();
            System.out.println("開始往這個buff寫入數據…");
            for (;;)// 模擬要處理很長時間    
            {
                if (System.currentTimeMillis()
                        - startTime > Integer.MAX_VALUE) {
                    break;
                }
            }
            System.out.println("終于寫完了");
        } finally {
            lock.unlock();
        }
    }

    public void read() throws InterruptedException {
        lock.lockInterruptibly();// 注意這里,可以響應中斷    
        try {
            System.out.println("從這個buff讀數據");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String args[]) {
        BufferInterruptibly buff = new BufferInterruptibly();

        final Writer2 writer = new Writer2(buff);
        final Reader2 reader = new Reader2(buff);

        writer.start();
        reader.start();

        new Thread(new Runnable() {

            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    if (System.currentTimeMillis()
                            - start > 5000) {
                        System.out.println("不等了,嘗試中斷");
                        reader.interrupt();
                        break;
                    }
                }
            }
        }).start();

    }
}

class Reader2 extends Thread {

    private BufferInterruptibly buff;

    public Reader2(BufferInterruptibly buff) {
        this.buff = buff;
    }

    @Override
    public void run() {

        try {
            buff.read();//可以收到中斷的異常,從而有效退出    
        } catch (InterruptedException e) {
            System.out.println("我不讀了");
        }

        System.out.println("讀結束");

    }
}

class Writer2 extends Thread {

    private BufferInterruptibly buff;

    public Writer2(BufferInterruptibly buff) {
        this.buff = buff;
    }

    @Override
    public void run() {
        buff.write();
    }
    
}


2個程序,運行結果:

run:
開始往這個buff寫入數據…
不等了,嘗試中斷

run:
開始往這個buff寫入數據…
不等了,嘗試中斷
我不讀了
讀結束

?ReentrantLock是一個互斥的同步器,其實現了接口Lock,里面的功能函數主要有:

  1. ?lock() -- 阻塞模式獲取資源
  2. ?lockInterruptibly() -- 可中斷模式獲取資源
  3. ?tryLock() -- 嘗試獲取資源
  4. tryLock(time) -- 在一段時間內嘗試獲取資源
  5. ?unlock() -- 釋放資源
    ReentrantLock實現Lock有兩種模式即公平模式和不公平模式

Concurrent包下的同步器都是基于AQS框架,在ReentrantLock里面會看到這樣三個類

static abstract class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
final boolean nonfairTryAcquire(int acquires) { ... }
protected final boolean tryRelease(int releases) { ... }
}


final static class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) { ... }
final void lock() { ... }
}


final static class FairSync extends Sync {
final void lock() { ... }
protected final boolean tryAcquire(int acquires) { ... }
}


再回歸到ReentrantLock對Lock的實現上

  1. ?ReentrantLock實例化
    ReentrantLock有個屬性sync,實際上對Lock接口的實現都是包裝了一下這個sync的實現
    如果是公平模式則創建一個FairSync對象,否則創建一個NonfairSync對象,默認是不公平模式
  2. lock() 調用sync.lock()
    公平模式下:直接走AQS的acquire函數,此函數的邏輯走一次tryAcquire,如果成功
    線程拜托同步器的控制,否則加入NODE鏈表,進入acquireQueued的tryAcquire,休眠,被喚醒的輪回
    不公平模式下和公平模式下邏輯大體上是一樣的,不同點有兩個:
    a. 在執行tryAcquire之前的操作,不公平模式會直接compareAndSetState(0, 1)原子性的設置AQS的資源
    0表示目前沒有線程占據資源,則直接搶占資源,不管AQS的NODE鏈表的FIFO原則
    b. tryAcquire的原理不一樣,不公平模式的tryAcquire只看compareAndSetState(0, 1)能否成功
    而公平模式還會加一個條件就是此線程對于的NODE是不是NODE鏈表的第一個
    c. 由于tryAcquire的實現不一樣,而公平模式和不公平模式在lock期間走的邏輯是一樣的(AQS的acquireQueued的邏輯)
    d. 對于一個線程在獲取到資源后再調用lock會導致AQS的資源做累加操作,同理線程要徹底的釋放資源就必須同樣
    次數的調用unlock來做對應的累減操作,因為對應ReentrantLock來說tryAcquire成功一個必須的條件就是compareAndSetState(0, 1)
    e. 由于acquireQueued過程中屏蔽了線程中斷,只是在線程拜托同步器控制后,如果記錄線程在此期間被中斷過則標記線程的
    中斷狀態
  3. ?lockInterruptibly() 調用sync.acquireInterruptibly(1),上一篇文章講過AQS的核心函數,這個過程和acquireQueued
    是一樣的,只不過在阻塞期間如果被標記中斷則線程在park期間被喚醒,然后直接退出那個輪回,拋出中斷異常
    由于公平模式和不公平模式下對tryAcquire的實現不一樣導致?lockInterruptibly邏輯也是不一樣
  4. tryLock() 函數只是嘗試性的去獲取一下鎖,跟tryAcquire一樣,這兩種模式下走的代碼一樣都是公平模式下的代碼
  5. tryLock(time) 調用sync.tryAcquireNanos(time),上一篇文章講過AQS的核心函數,這個過程和acquireQueued一樣,
    a. 在阻塞前會先計算阻塞的時間,進入休眠
    b. 如果被中斷則會判斷時間是否到了
    1. 如果沒到則且被其他線程設置了中斷標志,退出那個輪回,拋出中斷異常,如果沒有被設置中斷標記則是前一個線程
      釋放了資源再喚醒了它,其繼續走那個輪回,輪回中,如果tryAcquire成功則擺脫了同步器的控制,否則回到a
    2. 如果時間到了則退出輪回,獲取資源失敗
  6. ?unlock() 調用sync.release(1),上一篇文章講過AQS的核心函數,release函數會調用Sync實現的tryRelease函數來判斷
    釋放資源是否成功,即Sync.tryRelease函數,其邏輯過程是
    a. 首先判斷目前占據資源的線程是不是調用者,如果不是會拋出異常IllegalMonitorStateException
    b. 如果是則進行AQS資源的減1邏輯,如果再減1后AQS資源變成0則表示調用線程測得放棄了此鎖,返回給release的值的TRUE,
    release會喚醒下一個線程

整體來看ReentrantLock互斥鎖的實現大致是

  1. 自己實現AQS的tryAcquire和tryRelease邏輯,tryAcquire表示嘗試去獲取鎖,tryRelease表示嘗試去釋放鎖
  2. ReentrantLock對lock(),trylock(),trylock(time),unlock()的實現都是使用AQS的框架,然后AQS的框架又返回調用
    ReentrantLock實現的tryAcquire和tryRelease來對線程是否獲取鎖和釋放鎖成功做出依據判斷
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容