Java併發——ReentrantLock

簡介

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

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方法中,會再次嘗試獲取鎖,在這段時間若是該鎖被成功釋放,就能夠直接獲取鎖而不用掛起,其完整流程:ide

圖片來自 佔小狼——深刻淺出ReentrantLock

公平鎖與非公平鎖

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

  • 非公平鎖
  • ReentrantLock默認採用非公平鎖(組合方式)ui

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

    實現非公平鎖的核心方法nonfairTryAcquire(),其源碼以下:spa

    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有參構造方法傳入truecode

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

    實現公平鎖的核心方法tryAcquire(),其源碼以下:cdn

    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

    相關文章
    相關標籤/搜索