Java中的顯示鎖ReentrantLock使用與原理

考慮一個場景,輪流打印0-100之內的技術和偶數。經過使用 synchronize 的 wait,notify機制就能夠實現,核心思路以下:
使用兩個線程,一個打印奇數,一個打印偶數。這兩個線程會共享一個數據,數據每次自增,當打印奇數的線程發現當前要打印的數字不是奇數時,執行等待,不然打印奇數,並將數字自增1,對於打印偶數的線程也是如此java

//打印奇數的線程
private static class OldRunner implements Runnable{
    private MyNumber n;

    public OldRunner(MyNumber n) {
        this.n = n;
    }

    public void run() {
        while (true){
            n.waitToOld();  //等待數據變成奇數
            System.out.println("old:" + n.getVal());
            n.increase();
            if (n.getVal()>98){
                break;
            }
        }
    }
}
//打印偶數的線程
private static class EvenRunner implements Runnable{
    private MyNumber n;

    public EvenRunner(MyNumber n) {
        this.n = n;
    }

    public void run() {
        while (true){
            n.waitToEven();            //等待數據變成偶數
            System.out.println("even:"+n.getVal());
            n.increase(); 
            if (n.getVal()>99){
                break;
            }
        }
    }
}
複製代碼

共享的數據以下bash

private static class MyNumber{
    private int val;

    public MyNumber(int val) {
        this.val = val;
    }

    public int getVal() {
        return val;
    }
    public synchronized void increase(){
        val++;
        notify(); //數據變了,喚醒另外的線程
    }
    public synchronized void  waitToOld(){
        while ((val % 2)==0){
            try {
                System.out.println("i am "+Thread.currentThread().getName()+" ,but now is even:"+val+",so wait");
                wait(); //只要是偶數,一直等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void waitToEven(){
        while ((val % 2)!=0){
            try {
                System.out.println("i am "+Thread.currentThread().getName()+" ,but now old:"+val+",so wait");
                wait(); //只要是奇數,一直等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
複製代碼

運行代碼以下ui

MyNumber n = new MyNumber(0);
Thread old=new Thread(new OldRunner(n),"old-thread");
Thread even = new Thread(new EvenRunner(n),"even-thread");
old.start();
even.start();
複製代碼

運行結果以下this

i am old-thread ,but now is even:0,so wait
even:0
i am even-thread  ,but now old:1,so wait
old:1
i am old-thread ,but now is even:2,so wait
even:2
i am even-thread  ,but now old:3,so wait
old:3
i am old-thread ,but now is even:4,so wait
even:4
i am even-thread  ,but now old:5,so wait
old:5
i am old-thread ,but now is even:6,so wait
even:6
i am even-thread  ,but now old:7,so wait
old:7
i am old-thread ,but now is even:8,so wait
even:8
複製代碼

上述方法使用的是 synchronize的 wait notify機制,一樣可使用顯示鎖來實現,兩個打印的線程仍是同一個線程,只是使用的是顯示鎖來控制等待事件spa

private static class MyNumber{
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private int val;

    public MyNumber(int val) {
        this.val = val;
    }

    public int getVal() {
        return val;
    }
    public  void increase(){
        lock.lock();
        try {
            val++;
            condition.signalAll(); //通知線程
        }finally {
            lock.unlock();
        }

    }
    public  void  waitToOld(){
        lock.lock();
        try{
            while ((val % 2)==0){
                try {
                    System.out.println("i am should print old ,but now is even:"+val+",so wait");
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            lock.unlock();
        }
    }
    public void waitToEven(){
        lock.lock(); //顯示的鎖定
        try{
            while ((val % 2)!=0){
                try {
                    System.out.println("i am should print even ,but now old:"+val+",so wait");
                    condition.await();//執行等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }finally {
            lock.unlock(); //顯示的釋放
        }

    }
}
複製代碼

一樣能夠獲得上述的效果操作系統

顯示鎖的功能

顯示鎖在java中經過接口Lock提供以下功能 線程

  • lock: 線程沒法獲取鎖會進入休眠狀態,直到獲取成功
  • lockInterruptibly: 若是獲取成功,當即返回,不然一直休眠到線程被中斷或者是獲取成功
  • tryLock:不會形成線程休眠,方法執行會當即返回,獲取到了鎖,返回true,不然返回false
  • tryLock(long time, TimeUnit unit) throws InterruptedException : 在等待時間內沒有發生過中斷,而且沒有獲取鎖,就一直等待,當獲取到了,或者是線程中斷了,或者是超時時間到了這三者發生一個就返回,並記錄是否有獲取到鎖
  • unlock:釋放鎖
  • newCondition:每次調用建立一個鎖的等待條件,也就是說一個鎖能夠擁有多個條件

Condition的功能

接口Condition把Object的監視器方法wait和notify分離出來,使得一個對象能夠有多個等待的條件來執行等待,配合Lock的newCondition來實現。3d

  • await:使當前線程休眠,不可調度。這四種狀況下會恢復 1:其它線程調用了signal,當前線程剛好被選中了恢復執行;2: 其它線程調用了signalAll;3:其它線程中斷了當前線程 4:spurious wakeup (假醒)。不管什麼狀況,在await方法返回以前,當前線程必須從新獲取鎖
  • awaitUninterruptibly:使當前線程休眠,不可調度。這三種狀況下會恢復 1:其它線程調用了signal,當前線程剛好被選中了恢復執行;2: 其它線程調用了signalAll;3:spurious wakeup (假醒)。
  • awaitNanos:使當前線程休眠,不可調度。這四種狀況下會恢復 1:其它線程調用了signal,當前線程剛好被選中了恢復執行;2: 其它線程調用了signalAll;3:其它線程中斷了當前線程 4:spurious wakeup (假醒)。5:超時了
  • await(long time, TimeUnit unit) :與awaitNanos相似,只是換了個時間單位
  • awaitUntil(Date deadline):與awaitNanos類似,只是指定日期以後返回,而不是指定的一段時間
  • signal:喚醒一個等待的線程
  • signalAll:喚醒全部等待的線程

ReentrantLock

從源碼中能夠看到,ReentrantLock的全部實現全都依賴於內部類Sync和ConditionObject。
Sync自己是個抽象類,負責手動lock和unlock,ConditionObject則實如今父類AbstractOwnableSynchronizer中,負責await與signal Sync的繼承結構以下code

Sync的兩個實現類,公平鎖和非公平鎖

公平的鎖會把權限給等待時間最長的線程來執行,非公平則獲取執行權限的線程與線程自己的等待時間無關cdn

默認初始化ReentrantLock使用的是非公平鎖,固然能夠經過指定參數來使用公平鎖

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

當執行獲取鎖時,實際就是去執行 Sync 的lock操做:

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

對應在不一樣的鎖機制中有不一樣的實現

  1. 公平鎖實現
    final void lock() {
        acquire(1);
    }
    複製代碼
  2. 非公平鎖實現
    final void lock() {
        if (compareAndSetState(0, 1)) //先看當前鎖是否是已經被佔有了,若是沒有,就直接將當前線程設置爲佔有的線程
            setExclusiveOwnerThread(Thread.currentThread());
        else        
            acquire(1); //鎖已經被佔有的狀況下,嘗試獲取
    }
    複製代碼

兩者都調用父類AbstractQueuedSynchronizer的方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //一旦搶失敗,就會進入隊列,進入隊列後則是依據FIFO的原則來執行喚醒
        selfInterrupt();
}
複製代碼

當執行unlock時,對應方法在父類AbstractQueuedSynchronizer中

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
複製代碼

公平鎖和非公平鎖則分別對獲取鎖的方式tryAcquire 作了實現,而tryRelease的實現機制則都是同樣的

公平鎖實現tryAcquire

源碼以下

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); //獲取當前的同步狀態
    if (c == 0) {
        //等於0 表示沒有被其它線程獲取過鎖
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //hasQueuedPredecessors 判斷在當前線程的前面是否是還有其它的線程,若是有,也就是鎖sync上有一個等待的線程,那麼它不能獲取鎖,這意味着,只有等待時間最長的線程可以獲取鎖,這就是是公平性的體現
            //compareAndSetState 看當前在內存中存儲的值是否是真的是0,若是是0就設置成accquires的取值。對於JAVA,這種須要直接操做內存的操做是經過unsafe來完成,具體的實現機制則依賴於操做系統。
            //存儲獲取當前鎖的線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        //判斷是否是當前線程獲取的鎖
        int nextc = c + acquires;
        if (nextc < 0)//一個線程可以獲取同一個鎖的次數是有限制的,就是int的最大值
            throw new Error("Maximum lock count exceeded");
        setState(nextc); //在當前的基礎上再增長一次鎖被持有的次數
        return true;
    }
    //鎖被其它線程持有,獲取失敗
    return false;
}
複製代碼

非公平鎖實現tryAcquire

獲取的關鍵實現爲nonfairTryAcquire,源碼以下

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //鎖沒有被持有
        //能夠看到這裏會無視sync queue中是否有其它線程,只要執行到了當前線程,就會去獲取鎖
        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;
}
複製代碼

釋放鎖的機制

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread()) //只能是線程擁有這釋放
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //當佔有次數爲0的時候,就認爲全部的鎖都釋放完畢了
        free = true; 
        setExclusiveOwnerThread(null);
    }
    setState(c); //更新鎖的狀態
    return free;
}
複製代碼

從源碼的實現能夠看到

  • ReentrantLock獲取鎖時,在鎖已經被佔有的狀況下,若是佔有鎖的線程是當前線程,那麼容許重入,即再次佔有,若是由其它線程佔有,則獲取失敗,因而可知,ReetrantLock自己對鎖的持有是可重入的,同時是線程獨佔的
  • 公平與非公平就體如今,當執行的線程去獲取鎖的時候,公平的會去看是否有等待時間比它更長的,而非公平的就優先直接去佔有鎖

ReentrantLock的tryLock()與tryLock(long timeout, TimeUnit unit):

public boolean tryLock() {
//本質上就是執行一次非公平的搶鎖
   return sync.nonfairTryAcquire(1); 
}
複製代碼

有時限的tryLock核心代碼是 sync.tryAcquireNanos(1, unit.toNanos(timeout));,因爲有超時時間,它會直接放到等待隊列中,他與後面要講的AQS的lock原理中acquireQueued的區別在於park的時間是有限的,詳見源碼 AbstractQueuedSynchronizer.doAcquireNanos

爲何須要顯示鎖

內置鎖功能上有必定的侷限性,它沒法響應中斷,不能設置等待的時間

相關文章
相關標籤/搜索