Java併發——ReentrantLock

午夜12點發表於2018-07-30

簡介

ReentrantLock即可重入鎖(當前執行緒獲取該鎖再次獲取不會被阻塞),是一種遞迴無阻塞的同步機制。ReentrantLock基於AQS來實現,相對於內建鎖synchronized關鍵字功能更強大,多了等待可中斷、公平性、繫結多個條件等機制,還可以tryLock()避免死鎖,而若單獨從效能角度出發,更推薦synchronized

ReentrantLock

鎖獲取鎖流程:

lock方法:


    public void lock() {
        sync.lock();
    }
複製程式碼

Sync為ReentrantLock裡面的一個內部類,它繼承AQS,它有兩個子類:公平鎖FairSync和非公平鎖NonfairSync,ReentrantLock裡面大部分的功能都是委託給Sync來實現的,以非公平鎖為例其lock()方法


    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
複製程式碼

若鎖未執行緒佔有,把同步器中的exclusiveOwnerThread設定為當前執行緒
若鎖已有執行緒佔有,nonfairTryAcquire方法中,會再次嘗試獲取鎖,在這段時間如果該鎖被成功釋放,就可以直接獲取鎖而不用掛起,其完整流程:

Java併發——ReentrantLock
圖片來自佔小狼——深入淺出ReentrantLock

公平鎖與非公平鎖

公平鎖與非公平鎖的區別在於獲取鎖的時候是否按照FIFO的順序。

  • 非公平鎖
  • ReentrantLock預設採用非公平鎖(組合方式)

    
        public ReentrantLock() {
            sync = new NonfairSync();
        }
    複製程式碼

    實現非公平鎖的核心方法nonfairTryAcquire(),其原始碼如下:

    
        final boolean nonfairTryAcquire(int acquires) {
                //獲取當前執行緒
                final Thread current = Thread.currentThread();
                //獲取同步狀態
                int c = getState();
                // 若同步狀態為0,表明該鎖未被任何執行緒佔有
                if (c == 0) {
                    // CAS設定同步狀態
                    if (compareAndSetState(0, acquires)) {
                        // 設定鎖的擁有執行緒
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                // 檢查佔有執行緒是否是當前執行緒,可重入性關鍵程式碼
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    複製程式碼

    其主要邏輯:判斷同步狀態是否為0,若為0表明該鎖未被任何執行緒佔有,CAS設定同步狀態;若不為0表明該鎖已被執行緒佔有,判斷鎖佔有執行緒是否是當前執行緒,若是增加同步狀態(可重入性機制實現的關鍵)

  • 公平鎖
  • 公平鎖,通過ReentrantLock有參構造方法傳入true

    
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    複製程式碼

    實現公平鎖的核心方法tryAcquire(),其原始碼如下:

    
        protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0)
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
        }
    複製程式碼

    可以很明顯地發現與nonfairTryAcquire()方法唯一的區別在於CAS設定嘗試設定state值之前,呼叫了hasQueuedPredecessors()判斷當前執行緒是否位於CLH同步佇列中的第一個,若不是先執行完同步佇列中結點的執行緒,當前執行緒進入等待狀態

    
        public final boolean hasQueuedPredecessors() {
            Node t = tail; // Read fields in reverse initialization order
            Node h = head;
            Node s;
            return h != t &&
                ((s = h.next) == null || s.thread != Thread.currentThread());
        }
    複製程式碼
  • 公平鎖與非公平鎖
  • 公平鎖每次獲取到鎖為同步佇列中的第一個節點,符合請求資源時間上的絕對順序,而非公平鎖可能使執行緒"飢餓",有些執行緒可能一直獲取不到鎖,而剛釋放鎖的執行緒可能再次獲得該鎖,也正因為如此非公平鎖會降低一定的上下文切換,降低效能開銷,公平鎖為了保證時間上的絕對順序,需要頻繁的上下文切換。所以ReentrantLock預設採用非公平鎖保證系統更大的吞吐量

    可重入性

    可重入性需要解決以下兩個問題:

    ①.執行緒再次獲取鎖:鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒,如果是則再次成功獲取 次成功獲取
    ②.鎖的最終釋放:執行緒重複n次獲取了鎖,只有在n次釋放該鎖後,其他執行緒才能獲取到該鎖

    在nonfairTryAcquire()、tryAcquire()方法中都有這段程式碼:

    
        if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
    複製程式碼

    為了支援可重入性,若同步狀態不為0時,還會再判斷鎖持有執行緒是否是當前請求執行緒,若是再次獲取該鎖,同步狀態加1。再來看看釋放鎖:

    
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 同步狀態為0時,鎖才能釋放,將其持有執行緒置為null
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    複製程式碼

    只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否為0作為最終釋放的條件,當同步狀態為0時,將佔有執行緒設定為null,並返回true,表示釋放成功。

    繫結多個條件

    每一個Lock可以有任意資料的Condition物件,Condition是與Lock繫結的。Condition介面定義的方法,await對應於Object.wait,signal對應於Object.notify,signalAll對應於Object.notifyAll。

    生產者消費者簡單demo

    
        public class Resource {
    
        private int num = 1;//當前數量
    
        private int maxNum = 10;//極值
    
        private Lock lock = new ReentrantLock();
    
        private Condition productCon = lock.newCondition();
    
        private Condition consumerCon = lock.newCondition();
    
        public void product() {
            lock.lock();
            try {
                while (num >= maxNum) {
                    try {
                        System.out.println("當前已滿");
                        productCon.await();
                    } catch (InterruptedException e) {
    
                    }
                }
                num++;
                System.out.println("生產者" + Thread.currentThread().getName() + "當前有" + num + "個");
                consumerCon.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public void consume() {
            lock.lock();
            try {
                while (num == 0) {
                    try {
                        System.out.println("當前已空");
                        consumerCon.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                num--;
                System.out.println("消費者" + Thread.currentThread().getName() + "當前有" + num + "個");
                productCon.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            final Resource r = new Resource();
            // 生產者
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        r.product();
                    }
                }
            }).start();
            // 消費者
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        r.consume();
                    }
                }
            }).start();
        }
    }
    複製程式碼

    感謝

    《java併發程式設計的藝術》
    https://www.jianshu.com/p/4358b1466ec9

    相關文章