【Java併發】ReentrantLock

前篇寫了JUC的基礎AQS,其中介紹了它提供的很多模板方法,但是在實際編程中我們不會直接使用它,而是會使用它的各種實現。本文將介紹在實際使用中出現頻率很高的一種併發鎖——ReentrantLock。

從名字上來看,ReentrantLock具有兩個特性,一個是可重入一個是鎖

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
    }

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        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;
        }
    }

    public ReentrantLock() {
        sync = new NonfairSync();
    }

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

    public void lock() {
        sync.lock();
    }

    public void unlock() {
        sync.release(1);
    }
}

ReentrantLock內容定義了一個抽象類Sync,繼承自AQS,而不是自己去繼承AQS,所有對ReentrantLock的操作都會轉化爲對Sync的操作。同時又定義了Sync的兩個子類FairSync和NonfairSync,分別用於實現公平鎖和非公平鎖。除非你在生成ReentrantLock時顯示的指明需要公平鎖,否則默認採用非公平鎖。

可重入

先來看下可重入如何實現,以默認的非公平鎖舉例。可重入,意味着線程在獲取鎖之後,還可以再次獲取鎖,同樣,獲取了多少次,就要釋放多少次,否則資源釋放不對,別的線程將永遠無法獲得鎖。

final void lock() {
    //1、CAS將state置爲1
    if (compareAndSetState(0, 1))
        //2、設置自己爲獨佔線程
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //3、否則嘗試獲取資源
        acquire(1);
}

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //1、如果state爲0,說明當前鎖沒有被佔用
            if (c == 0) {
                //2、CAS嘗試將state設爲1
                if (compareAndSetState(0, acquires)) {
                    //3、獲取鎖成功,設置自己爲獨佔線程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //4、如果持有當前鎖的線程就是自己
            else if (current == getExclusiveOwnerThread()) {
                //5、那麼將state增加acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //6、更新state,因爲鎖已經被自己持有了,所以這裏不需要CAS設置
                setState(nextc);
                return true;
            }
            return false;
}


//AQS
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

獲取鎖的過程中,4、5、6三步即實現了可重入獲取。再看下釋放。

protected final boolean tryRelease(int releases) {
            //1、每次釋放,將state減去釋放數
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //2、如果state爲0,說明所有資源都已經釋放
            if (c == 0) {
                free = true;
                //3、將獨佔線程置空
                setExclusiveOwnerThread(null);
            }
            //4、更新state
            setState(c);
            return free;
        }

同樣,釋放時需要將獲取的資源依次扣除,什麼時候state減爲0了,纔算該線程持有的所有資源都釋放掉了。

公平鎖實現可重入同理,不再贅述。

公平與非公平

那麼公平鎖與非公平鎖又有什麼區別呢?

非公平鎖的獲取上面已經分析了,來看下公平鎖的獲取,看下有什麼不同。

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //1、等待隊列中是否已經有節點在等待獲取鎖了
                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;
        }

public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        //2、等待隊列以後已經初始化,並且有別的線程正在入隊(enq)或者已經入隊
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

公平和非公平的區別就在於,嘗試獲取鎖前要看下是否有線程已經在自己之前就開始等待了,如果沒有才去競爭。通過這種方式保證公平,即先等先得。

公平鎖和非公平鎖哪種性能更好呢,公平鎖雖然能保證等待最久的線程可以先獲得鎖,但是這同時也以爲着每次都會是不同的線程獲取鎖,每次都要進行線程切換。《Java併發編程的藝術》進行了測試,表明非公平鎖雖然可能造成某些線程一直獲取不到鎖,但是可以提高整體的吞吐量,所以ReentrantLock將其作爲了默認實現。如果是需要保證這種先等先得的特性,也可以使用公平鎖。

與synchronized對比

ReentrantLock在加鎖和內存語義上提供了與synchronized相同的功能,此外還提供了定時、可中斷、公平性等特性。JDK5時,ReentrantLock的性能要遠優於synchronized,但是JDK6引入了synchronized的鎖優化,兩者之間的差別並沒有那麼大了。

《Java併發編程實戰》作者建議,除非需要一些高級功能,如可定時、可輪詢、可中斷、公平性等,才使用ReentrantLock,否則應該優先使用synchronized,畢竟大部分人都用習慣了,而且使用簡單。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章