Basic Of Concurrency(十三: Slipped Conditions)

什麼是Slipped Conditions?

Slipped Conditions是指一個線程對一個確切的條件進行檢查到操做期間,若是條件被其餘線程訪問到的話就會給第一個線程的執行結果形成影響。下面是一個簡單的實例:html

public class Lock {
    private boolean isLocked = false;

    public void lock() {
        synchronized (this) {
            while (isLocked) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        synchronized (this) {
            isLocked = true;
        }
    }

    public synchronized void unLock() {
        isLocked = false;
        notify();
    }
}
複製代碼

咱們能夠注意到lock()方法中有兩個同步塊。第一個同步塊會讓線程一直等待直到isLocked爲false爲止。第二個同步塊用來設置isLocked爲true,以便讓當前線程取得Lock實例阻塞其餘線程進入臨界區。java

想象一下當isLocked爲false時,有兩個線程同時調用lock()方法。若是第一個線程搶先進入到第一個同步塊中,它會檢查isLocked並發現它爲false並退出同步塊.若是這時第二個線程恰好被容許執行進入第一個同步塊,它一樣會檢查isLocked並發現它爲false。這樣兩個線程同時讀取到條件爲false。而後兩個線程都會進入到第二個同步塊,設置isLocked爲true並繼續運行。問題在於第二個線程在第一個線程檢查和設置isLocked之間的時間點就訪問了isLocked.以致於最後兩個線程都能退出lock()方法執行到臨界區的代碼.併發

這種狀況咱們稱爲slipped conditions.全部的線程都能在搶先運行的線程改變條件前訪問並檢查條件,從而退出同步代碼塊.換句話說,條件的訪問時機被拉長了.條件在被線程改變前,其餘線程都可以訪問到它.post

爲了解決Slipped Conditions問題,咱們須要讓線程以原子的方式來檢查和設置條件,這樣才能保證線程在執行檢查和設置的過程當中不會有線程可以訪問到條件。優化

解決上文例子中提到的問題比較簡單,只須要將isLocked=true;這一行移動到第一個同步塊while()循環下方便可。以下所示:this

public class Lock {
    private boolean isLocked = false;

    public void lock() {
        synchronized (this) {
            while (isLocked) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            isLocked = true;
        }
    }

    public synchronized void unLock() {
        isLocked = false;
        notify();
    }
}
複製代碼

如今咱們能夠看到isLocked的檢查和設置都被放在同一個同步代碼塊中來保證原子操做。spa

一個更加完整的實例

也許你覺的你永遠不會實現一個像上文中提到的一摸同樣的Lock,因此對Slipped Conditions問題的出現存在爭議.以爲它只是理論上的問題.但實際它是真實發生的.上文提到的實例是爲凸顯Slipped Conditions問題而精簡設計的. 一個更加完整和真實的實例是實現一個FairLock.FairLock的實如今飢餓與公平一文中有說起.讓咱們回頭看Nested Monitor Lockout問題,在解決它的過程當中很容易遇到Slipped Conditons問題.首先咱們看一個有nested monitor lockout問題的實例.線程

public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread;
    private List<QueueObject> waitingThreads = new ArrayList();

    public void lock() throws InterruptedException {
        QueueObject queueObject = new QueueObject();

        synchronized (this) {
            waitingThreads.add(queueObject);
            while (isLocked || waitingThreads.get(0) != queueObject) {
                synchronized (queueObject) {
                    try {
                        queueObject.wait();
                    } catch (InterruptedException e) {
                        waitingThreads.remove(queueObject);
                        throw e;
                    }
                }
            }
            waitingThreads.remove(queueObject);
            isLocked = true;
            lockingThread = Thread.currentThread();
        }
    }

    public synchronized void unLock() {
        if (this.lockingThread != Thread.currentThread()) {
            throw new IllegalMonitorStateException("Calling thread has not locked this lock");
        }
        isLocked = false;
        lockingThread = null;
        if (waitingThreads.size() > 0) {
            QueueObject queueObject = waitingThreads.get(0);
            synchronized (queueObject) {
                queueObject.notify();
            }
        }
    }

複製代碼

咱們會注意到synchronized(queueObject)同步塊連同同步塊中的queueObject.wait()調用都被嵌套在synchronized(this)中.這將產生nested monitor lockout問題.爲了解決這個問題,咱們須要將synchronized(queueObject)同步代碼塊在synchronized(this)同步代碼塊中移除.以下所示:設計

public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread;
    private List<QueueObject> waitingThreads = new ArrayList();

    public void lock() throws InterruptedException {
        QueueObject queueObject = new QueueObject();

        synchronized (this) {
            waitingThreads.add(queueObject);
        }

        boolean mustWait = true;
        while (mustWait) {
            synchronized (this) {
                mustWait = isLocked || waitingThreads.get(0) != queueObject;
            }
            synchronized (queueObject) {
                if (mustWait) {
                    try {
                        queueObject.wait();
                    } catch (InterruptedException e) {
                        waitingThreads.remove(queueObject);
                        throw e;
                    }
                }
            }
        }
        synchronized(this) {
            waitingThreads.remove(queueObject);
            isLocked = true;
            lockingThread = Thread.currentThread();
        }
    }
}
複製代碼

注意:咱們只修改lock()方法,所以咱們只看lock()方法的改動便可.code

咱們能夠注意到此時lock()方法中有四個同步代碼塊.咱們能夠先忽略如下代碼塊:

synchronized(this){
      waitingThreads.add(queueObject);
    }
複製代碼

除去這裏示例的代碼塊,一共還有3個.

第一個synchronized(this)同步代碼塊中用於檢查mustWait = isLocked || waitingThreads.get(0) != queueObject.表達式的值.
第二個synchronized(queueObject)同步代碼塊用於檢查線程是否須要調用queueObject.wait()以進入等待狀態.在此期間上一個線程可能尚未取得FairLock實例.不過咱們能夠先忽略這一點.如今咱們須要關注的是當前FairLock實例尚未被鎖住,因此線程能夠退出sychronized(queueObject)同步代碼塊.

第三個synchronized(this)同步代碼塊只有在mustWait = false時才能被訪問到.它將條件isLocked設置回true而且退出lock()方法調用.

當FairLock未被鎖住的狀況下,有兩個線程同時調用lock()方法.首先線程1檢查isLocked發現它爲false.線程2也是如此.而後兩個線程不會進入等待狀態而是同時設置isLocked狀態爲true.這是一個典型的slipped conditions實例.

解決Slipped Conditions問題

爲了解決上文實例的slipped conditions問題,咱們須要將最後一個synchronized(this)同步塊中的內容移動到第二個同步塊中去.原先的代碼天然須要作出一點小改動來適應此次移動.看起來是這樣的:

// 當前FairLock沒有nested monitor lockout問題
// 當仍然有信號丟失問題
public class FairLock {
    private boolean isLocked = false;
    private Thread lockingThread;
    private List<QueueObject> waitingThreads = new ArrayList();

    public void lock() throws InterruptedException {
        QueueObject queueObject = new QueueObject();

        synchronized (this) {
            waitingThreads.add(queueObject);
        }

        boolean mustWait = true;
        while (mustWait) {
            synchronized (this) {
                mustWait = isLocked || waitingThreads.get(0) != queueObject;
                
                // 移動代碼塊,start
                if(!mustWait){
                    waitingThreads.remove(queueObject);
                    isLocked = true;
                    lockingThread = Thread.currentThread();
                    return;
                }
                // 移動代碼塊,end
            }
            synchronized (queueObject) {
                if (mustWait) {
                    try {
                        queueObject.wait();
                    } catch (InterruptedException e) {
                        waitingThreads.remove(queueObject);
                        throw e;
                    }
                }
            }
        }
    }
}
複製代碼

如今咱們注意到mustWait條件表達式的檢查和設置被放置在同一個同步代碼塊中.同時須要注意的是,儘管mustWait本地變量在synchronized(this)外部被while(mustWait)看成條件使用,但mustWait的值始終沒有在外部被更改.一旦線程解析到mustWait的值爲false時,會在同一個原子操做中將mustWait設置爲true.以便讓其餘線程在解析條件表達式時獲得true值.

return;語句在synchronized(this)同步塊中並非必須的.這只是一個小優化.若是線程已經知道mustWait=false,則沒有必要繼續往下執行,進入synchronized (queueObject)同步代碼塊再去判斷mustWait=false後退出.這有點相似快速失敗.

若是你善於觀察的話,仍然會發現當前FairLock實現會有信號丟失問題(你不看代碼也會看註釋吧,哈哈...).跟以前的信號丟失問題同樣,若mustWait=true.則調用線程將會進入synchronized (queueObject)同步代碼塊準備調用queueObject.wait();若此時其餘線程搶先調用unLock()方法並進入unLock()中的synchronized (queueObject)同步代碼塊中成功調用了queueObject.notify(),則此調用會失效而且信號丟失,由於在queueObject對象鎖上尚未任何線程調用wait()方法等待notify()的喚醒.這樣線程lock()方法中的線程在信號丟失後再進入synchronized (queueObject)同步代碼調用wait()方法,可能會永遠等待下去,除非有其餘線程再次調用了當前queueObject的notify()方法.

信號丟失問題已經在以前的飢餓與公平一文中給出瞭解決方法.只須要將QueueObject.class替換成一個Semaphore.class便可.將queueObject的wait()和notify()方法調用替換成Semaphore的doWait()和doNotify()調用便可.這些調用會將信號存儲在Semaphore對象中.這樣就算doNotify()在doWait()以前調用了也不會有信號丟失問題.

該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial

上一篇: Nested Monitor Lockout
下一篇: Java中的鎖

相關文章
相關標籤/搜索