1. 程式人生 > >Java中的顯示鎖ReentrantLock使用與原理

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

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

//列印奇數的執行緒
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;
            }
        }
    }
}

共享的資料如下

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();
            }
        }
    }
}

執行程式碼如下

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();

執行結果如下

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機制,同樣可以使用顯示鎖來實現,兩個列印的執行緒還是同一個執行緒,只是使用的是顯示鎖來控制等待事件

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來實現。

  • 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的繼承結構如下

在這裡插入圖片描述
Sync的兩個實現類,公平鎖和非公平鎖

公平的鎖會把許可權給等待時間最長的執行緒來執行,非公平則獲取執行許可權的執行緒與執行緒本身的等待時間無關

預設初始化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

為什麼需要顯示鎖

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