一步一步建立JAVA線程

(一)建立線程html

要想明白線程機制,咱們先從一些基本內容的概念下手。java

線程和進程是兩個徹底不一樣的概念,進程是運行在本身的地址空間內的自包容的程序,而線程是在進程中的一個單一的順序控制流,所以,單個進程能夠擁有多個線程。算法

還有就是任務和線程的區別。線程彷佛是進程內的一個任務,準確點講,任務是由執行線程來驅動的,而任務是附着在線程上的。編程

1、如今正式講講線程的建立。canvas

正如咱們前面講的,任務是由執行線程驅動的,沒有附着任務的線程根本就不能說是線程,因此咱們在建立線程的時候,將任務附着到線程上。數組

所謂的任務,對應的就是Runnable,咱們要在這個類中編寫相應的run()方法來描述這個任務所要執行的命令,接着就是將任務附着到線程上。像是這樣:安全

Thread thread = new Thread(new Runnable(){
       @Override
       public void run(){
          ...
       }
});

接着咱們只要經過start()啓動該Thread就行。服務器

2、線程的啓動數據結構

一種方式就是上面使用的:建立一個Thread的子類,而後實現run()方法,接着一樣是經過start()來開啓它。注意,Thread的子類只能承載一個任務。多線程

另一種方式就是經過Executor(執行器)來實現。

Executor會在客戶端和任務之間提供一個間接層,由這個間接層來執行任務,而且容許管理異步任務的執行,而無需經過顯式的管理線程的生命週期。

ExecutorService exec = Executors.newCachedThreadPool();

exec.executor(new RunnableClass);

其中,CachedThreadPool是一種線程池。

3、線程池

線程池在多線程處理技術中是一個很是重要的概念,它會將任務添加到隊列中,而後在建立線程後自動啓動這些任務。線程池的線程都是後臺線程,每一個線程都是用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。線程池中的線程數目是有一個最大值,但這並不意味着只能運行這樣多的線程,它的真正意思是同時可以運行的最大線程數目,因此能夠等待其餘線程運行完畢後再啓動。

線程池都有一個線程池管理器,用於建立和管理線程池,還有一個工做線程,也就是線程池中的線程。咱們必須提供給線程池中的工做線程一個任務,這些任務都是實現了一個任務接口,也就是Runnable。線程池還有一個重要的組成:任務隊列,用於存放沒有處理的任務,這就是一種緩衝機制。

經過線程池的介紹,咱們能夠知道,使用到線程池的狀況就是這樣:須要大量的線程來完成任務,而且完成任務的時間比較短,就像是咱們如今的服務器,同時間接受多個請求而且處理這些請求。

java除了上面的CachedThreadPool,還有另外一種線程池:FixedThreadPool。CachedThreadPool會在執行過程當中建立與所需數量相同的線程,而後在它回收舊線程的時候中止建立新的線程,也就是說,它每次都要保證同時運行的線程的數量不能超過所規定的最大數目。而FixedThreadPool是一次性的預先分配所要執行的線程,像是這樣:

ExecutorService exec = Executors.newFixedThreadPool(5);

就是不管要分配的線程的數目是多少,都是運行5個線程。這樣的好處是很是明顯的,就是用於限制線程的數目。CachedThreadPool是按需分配線程,直到有的線程被回收,也就是出現空閒的時候纔會中止建立新的線程,這個過程對於內存來講,代價是很是高昂的,由於咱們不知道實際上須要建立的線程數量是多少,只會一直不斷建立新線程。

看上去彷佛FixedThreadPool比起CachedThreadPool更加好用,但實際上使用更多的是CachedThreadPool由於通常狀況下,不管是什麼線程池,現有線程都有可能會被自動複用,而CachedThreadPool在線程結束的時候就會中止建立新的線程,也就是說,它能確保結束掉的線程的確是結束掉了,不會被從新啓動,而FixedThreadPool沒法保證這點。

接下來咱們能夠看看使用上面兩種線程池的簡單例子:

public void main(String[] args){
     ExecutorService cachedExec = Executors.newCachedThreadPool();
     for(int i = 0; i < 5; i++){
        cachedExec.execute(new RunnableClass);
     }
     cachedExec.shutdown();

     ExecutorService fixedExec = Executors.newFixedThreadPool(3);
     for(int i = 0; i < 5; i++){
         fixedExec.execute(new RunnableClass);
     }
     fixedExec.shutdown();
}

CachedThreadPool會不斷建立線程直到有線程空閒下來爲止,而FixedThreadPool會用3個線程來執行5個任務。
在java中,還有一種執行線程的模式:SingleThreadExecutor。顧名思義,該執行器只有一個線程。它就至關於數量爲1的FixedThreadPool,若是咱們向它提交多個任務,它們就會按照提交的順序排隊,直到上一個任務執行完畢,由於它們就只有一個線程能夠運行。這種方式是爲了防止競爭,由於任什麼時候刻都只有一個任務在運行,從而不須要同步共享資源。

 

(二)Thread的生命週期

以前講到Thread的建立,那是Thread生命週期的第一步,其後就是經過start()方法來啓動Thread,它會執行一些內部的管理工做而後調用Thread的run()方法,此時該Thread就是alive(活躍)的,並且咱們還能夠經過isAlive()方法來肯定該線程是否啓動仍是終結。

一旦啓動Thread後,咱們就只能執行一個方法:run(),而run()方法就是負責執行Thread的任務,因此終結Thread的方法很簡單,就是終結run()方法。仔細查看文檔,咱們會發現裏面有一個方法:stop(),彷佛能夠用來中止Thread,可是這個方法已經被廢除了,由於它存在着內部的競爭。

咱們常常須要一個不斷執行的Thread,而後在某個特定的條件下才會終結它,方法有不少,但最經常使用的有設定標記和中斷Thread兩種方式。

咱們將以前例子中的Thread改寫一下:

public class RandomCharacterGenerator extends Thread implements CharacterSource {
    static char[] chars;
    static String charArray = "abcdefghijklmnopqrstuvwxyz0123456789";
    static {
        chars = charArray.toCharArray();
    }
    
    private volatile boolean done = false;

    Random random;
    CharacterEventHandler handler;

    public RandomCharacterGenerator() {
        random = new Random();
        handler = new CharacterEventHandler();
    }

    public int getPauseTime() {
        return (int) (Math.max(1000, 5000 * random.nextDouble()));
    }

    @Override
    public void addCharacterListener(CharacterListener cl) {
        handler.addCharacterListener(cl);
    }

    @Override
    public void removeCharacterListener(CharacterListener cl) {
        handler.removeCharacterListener(cl);
    }

    @Override
    public void nextCharacter() {
        handler.fireNewCharacter(this,
                (int) chars[random.nextInt(chars.length)]);
    }

    public void run() {
        while(!done){
            nextCharacter();
            try {
                Thread.sleep(getPauseTime());
            } catch (InterruptedException ie) {
                return;
            }
        }
    }
    
    public void setDone(){
        done = true;
    }
}

如今咱們多了一個標記:done,這樣咱們就能夠在代碼中經過調用setDone()來決定何時中止該Thread。這裏使用了volatile關鍵字,它主要是爲了同步。這點會放在同步這裏講。
設定標記的最大問題就是咱們必須等待標記的狀態,這樣就會形成延遲。固然,這種延遲是沒法避免的,但必須想辦法縮短到最小。因而,中斷Thread這種方法就有它的發揮地方了。

咱們能夠經過interrupt()方法來中斷Thread,該方法會形成兩個反作用:

1.它會致使任何的阻塞方法都會拋出InterruptedException,咱們必須強制性的捕獲這個錯誤哪怕咱們根本就不須要處理它,這也是java的異常處理機制讓人詬病的一個地方。

2.設定Thread對象內部的標記來指示此Thread已經被中斷了,像是這樣:

public void run(){

     while(!isInterrupted()){

        ...

      }

}

雖然沒法避免延遲,可是延遲已經被縮短了。
不管是採用標記仍是中斷的方法,咱們之因此沒法消除延遲的緣由是咱們沒法肯定是檢查標記先仍是調用方法先,這就是所謂的race condition,是線程處理中永遠沒法避免的話題。

Thread不只能夠被終結,還能夠暫停,掛起和恢復。

Thread本來有suspend()方法和resume()方法來執行掛起和恢復,但它們和stop()出於一樣的緣由,都被廢除了。

咱們能夠經過sleep()方法來掛起Thread,當在指定的時間後,它就會自動恢復。嚴格意義上講,sleep並不等同於suspend,真正的suspend應該是由一個線程來掛起另外一個線程,可是sleep只會影響當前的Thread。要想真正實現掛起和恢復,咱們可使用等待和通知機制,但這個機制最大的問題就是咱們的Thread必須使用該技術來編寫。

Thread在終結後,若是有可能,咱們還須要對它進行善後。即便Thread已經被終結了,可是其餘對象只要還持有它的引用,它們就能夠調用該Thread的資源,這也會致使該Thread沒法被回收。

但咱們有時候仍是但願繼續保持該Thread的引用,由於咱們想要判別它是否真的已經完成了工做,可使用join()方法。

join()方法會被阻塞住直到Thread完成它的run()方法,可是這個存在風險:第一次對join()方法的調用可能會一直被阻塞住很長時間直到Thread真正完成,因此,通常狀況下咱們仍是使用isAlive()方法來判斷。

因爲咱們能夠經過實現一個Runnable接口來定義咱們的任務,因此在判斷所在線程是否已經中斷的時候,就有一個問題:該任務尚未綁定到任何線程上。咱們能夠經過currentThread()方法來得到當前Thread的引用,接着調用isInterrupted()來判斷線程是否中斷。

 

(三)synchronized和volatile的使用

如今開始進入線程編程中最重要的話題---數據同步,它是線程編程的核心,也是難點,就算咱們理解了數據同步的基本原理,可是咱們也沒法保證可以寫出正確的同步代碼,但基本原理是必須掌握的。

要想理解數據同步的基本原理,首先就要明白,爲何咱們要數據同步?

public class CharacterDisplayCanvas extends JComponent implements
        CharacterListener {
    protected FontMetrics fm;
    protected char[] tmpChar = new char[1];
    protected int fontHeight;

    public CharacterDisplayCanvas() {
        setFont(new Font("Monospaced", Font.BOLD, 18));
        fm = Toolkit.getDefaultToolkit().getFontMetrics(getFont());
        fontHeight = fm.getHeight();
    }

    public CharacterDisplayCanvas(CharacterSource cs) {
        this();
        setCharacterSource(cs);
    }

    public void setCharacterSource(CharacterSource cs) {
        cs.addCharacterListener(this);
    }

    public synchronized void newCharacter(CharacterEvent ce) {
        tmpChar[0] = (char) ce.character;
        repaint();
    }

    public Dimension preferredSize() {
        return new Dimension(fm.getMaxAscent() + 10, fm.getMaxAdvance() + 10);
    }

    protected synchronized void paintComponent(Graphics gc) {
        Dimension d = getSize();
        gc.clearRect(0, 0, d.width, d.height);
        if (tmpChar[0] == 0) {
            return;
        }
        int charWidth = fm.charWidth((int) tmpChar[0]);
        gc.drawChars(tmpChar, 0, 1, (d.width - charWidth) / 2, fontHeight);
    }
}

仔細查看上面的代碼,咱們就會發現,有兩個方法的前面多了一個新的關鍵字:synchronized。讓咱們看看這兩個方法爲何要添加這個關鍵字。
newCharacter()用於顯示新字母,而paintComponent()負責調整和重畫canvas。這兩個方法存在着race condition,也就是競爭,由於它們訪問的是同一份數據,最重要的是它們是由不一樣的線程所調用的,這就致使咱們沒法保證它們的調用是按照正確的順序來進行,可能在newCharacter()方法未被調用前paintComponent()方法就已經從新繪製canvas。

之因此產生競爭,除了這兩個方法訪問的是同一份數據以外,還和它們是非automic有關。一個程序若是被認爲是automic,那麼就表示它是沒法被中斷的,不會有中間狀態。使用synchronized,就能保證該方法沒法被中斷,那麼其餘線程就沒法在該方法沒有完成前調用它。

結合對象鎖的知識,咱們能夠簡單的講解一下synchronized的原理:一個線程若是想要調用另外一個線程的synchronized方法,並且該方法正在被其餘線程調用,那麼這個線程就必須等待,等待其餘線程釋放該方法所在的對象的鎖,而後得到該鎖執行該方法。鎖機制可以確保同一時間只有一個線程可以調用該方法,也就能保證只有一個線程可以訪問數據。

還記得咱們以前經過使用標記來結束線程的時候,將該標記用volatile修飾?若是咱們不用volatile,又能使用什麼方法呢?

若是單單只是上面的知識,咱們可能會想到利用synchronized來同步run()和setDone(),由於就是這兩個方法在競爭done這個數據。可是這樣存在很大的問題:run()會在done沒有被設置true前永遠不會結束,可是done標記卻要等到run()方法結束後才能由setDone()方法進行設置。

這就是一個死鎖,永遠解不開的鎖。

產生死鎖的緣由有不少,像是上面這種狀況就是一個典型的表明,主要緣由就是run()方法的scope(範圍)太大。所謂的scope,指的是獲取鎖到釋放鎖的時間,而run()方法的scope是一個循環,除非done設置爲true。這種須要依賴其餘線程的方法來結束執行的方法,若是將整個方法設置爲同步,就會出現死鎖。

因此,最好的方法就是將scope縮小。

咱們能夠不用對整個方法進行同步,而是對須要訪問的數據進行同步,也就是對done使用volatile。

要想理解volatile的工做原理,咱們必須清楚變量的加載機制。java的內存模型容許線程可以在local memory中持有變量的值,因此這也就致使某個線程改變該變量的值時,其餘線程可能不會察覺到該變量的變化。這種狀況只是一種可能,並不表明必定會出現,但像是循環執行這種操做,就增長了這種可能。

因此,咱們要作的事情其實很簡單,就是讓線程從同一個地方取出變量而不是本身維護一份。使用volatile,每次使用該變量都要從主存儲器中讀取,每次改變該變量時,也要存入主存儲器,並且加載和存儲都是automic,不管是不是long或者double變量(這兩種類型的存儲是非automic的)。

值得注意的,run()方法和setDone()方法自己就是automic,由於setDone()方法僅有一個存儲操做,而run()方法也只有一個讀取操做,其他部分根本就須要該值保持不變,也就是說,這兩個方法其實自己就不存在競爭。

但讓人更加困惑的是,volatile自己的存在如今也引發人們的關注:它到底有沒有必要?

volatile是以moot point(未決點)來實現的:變量永遠都從主存儲器中讀取,但這也只是JDK 1.2以前的狀況,如今的虛擬機實現使得內存模式愈來愈複雜,並且也獲得了極大的優化,而且這種趨勢只會一直持續下去。也就是說,基於內存模式的volatile可能會由於內存模式的不斷優化而逐漸變得沒有意義。

volatile的使用是有侷限的,它僅僅解決因內存模式而引起的問題,並且只能用在對變量的automic操做上,也就是訪問該變量的方法只能夠有單一的加載或者存儲。但不少方法都是非automic,像是遞增或者遞減操做,就容許存在中間狀態,由於它們自己就是載入,變動和存儲的簡化而已,也就是所謂的syntactic sugar(語法糖)。

咱們大概能夠這樣理解volatile的使用條件:強迫虛擬機不要臨時複製變量,哪怕咱們在許多狀況下都不會使用它們。

volatile是否能夠運用在數組上,讓整個數組中的全部元素都被同步呢?凡是使用java的人都會對這樣的幻想嗤之以鼻,由於實際狀況是隻有數組的引用纔會被同步,數組中的元素不會是volatile的,虛擬機仍是能夠將個別元素存儲於local的寄存器中,沒有任何方法能夠指定數組的元素應該以volatile的方式來處理。

咱們上面的同步問題是發生在展現隨機數字與字母的顯示組件,如今咱們繼續將功能完善:玩家能夠輸入所顯示的字母,而且正確就會得分。

(四)同步方法和同步塊

在以前例子的基礎上,咱們增長新的功能:根據正確與不正確的響應來顯示玩家的分數。

public class ScoreLabel extends JLabel implements CharacterListener {
    private volatile int score = 0;
    private int char2type = -1;
    private CharacterSource generator = null, typist = null;

    public ScoreLabel(CharacterSource generator, CharacterSource typist) {
        this.generator = generator;
        this.typist = typist;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public ScoreLabel() {
        this(null, null);
    }

    public synchronized void resetGenerator(CharacterSource newCharactor) {
        if (generator != null) {
            generator.removeCharacterListener(this);
        }
        generator = newCharactor;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
    }

    public synchronized void resetTypist(CharacterSource newTypist) {
        if (typist != null) {
            typist.removeCharacterListener(this);
            typist = newTypist;
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public synchronized void resetScore() {
        score = 0;
        char2type = -1;
        setScore();
    }

    private synchronized void setScore() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                setText(Integer.toString(score));
            }
        });
    }

    @Override
    public synchronized void newCharacter(CharacterEvent ce) {
        if (ce.source == generator) {
            if (char2type != -1) {
                score--;
                setScore();
            }
            char2type = ce.character;
        } else {
            if (char2type != ce.character) {
                score--;
            } else {
                score++;
                char2type = -1;
            }
            setScore();
        }
    }
}

這裏咱們將newCharacter()方法用synchronized進行同步,是由於這個方法會被多個線程調用,而咱們根本就不知道哪一個線程會在何時調用這個方法。這就是race condition。
變量的volatile沒法解決上面的多線程調度問題,由於這裏的問題是方法調度的問題,並且更加可怕的是,須要共享的變量很多,其中有些變量是做爲條件判斷,這就會致使在這些條件變量沒有正確的設置前,有些線程已經開始啓動了。

這並非簡單的將這些變量設置爲volatile就能解決的問題,由於就算這些變量的狀態不對,其餘線程依然可以啓動。

這裏有幾個方法的同步是須要引發咱們注意的:resetScore(),resetGenerator()和resetTypist()這幾個方法是在從新啓動時纔會被調用,彷佛咱們不須要爲此同步它們:其餘線程這時根本就沒有開始啓動!!

可是咱們仍是須要同步這些方法,這是一種防衛性的設計,保證整個Class全部相關的方法都是線程安全的。遺憾的是,咱們必須這樣考慮,由於多線程編程的最大問題就是咱們永遠也不知道咱們的程序會出現什麼問題,因此,任何可能會引發線程不安全的因素咱們都要儘可能避免。

這也就引出咱們的問題:如何可以對兩個不一樣的方法同步化以防止多個線程在調用這些方法的時候影響對方呢?

對方法作同步化,可以控制方法執行的順序,由於某個線程上已經運行的方法沒法被其餘線程調用。這個機制的實現是由指定對象自己的lock來完成的,由於方法須要訪問的對象的lock被一個線程佔有,但值得注意的是,所謂的對象鎖其實並非綁定在對象上,而是對象實例上,若是兩個線程擁有對象的兩個實例,它們均可以同時訪問該對象,

同步的方法如何和沒有同步的方法共同執行呢?

全部的同步方法都會執行獲取對象鎖的步驟,可是沒有同步的方法,也就是異步方法並不會這樣,因此它們可以在任意的時間點被任意的線程執行,而無論究竟是否有同步方法在執行。

關於對象鎖的話題天然就會引出一個疑問:靜態的同步方法呢?靜態的同步方法是沒法獲取對象鎖的,由於它沒有this引用,對於它的調用是不存在對象的。但靜態的同步方法的確是存在的,那麼它又是怎樣運做的呢?

這須要另外一個鎖:類鎖。

咱們能夠從對象實例上得到鎖,也能從class(由於class對象的存在)上得到鎖,即便這東西其實是不存在的,由於它沒法實現,只是幫助咱們理解的概念。值得注意的是,由於一個class只有一個class對象,因此一個class只有一個線程能夠執行同步的靜態方法,並且與對象的鎖毫無相關,類鎖能夠再對象鎖外被獨立的得到和釋放,一個非靜態的同步方法若是調用同步的靜態方法,那麼它能夠同時得到這兩個鎖。

提供synchronized關鍵字的目的是爲了讓對象中的方法可以循序的進入,大部分數據保護的需求均可以由這個關鍵字實現,但在更加複雜的同步化狀況中仍是太簡單了。

在java這個對象王國裏,難道真的是沒有Lock這個對象的容身之處嗎?答案固然是不可能的,J2SE 5.0開始提供Lock這個接口:

private Lock scoreLock = new ReentrantLock();

public void newCharacter(CharacterEvent ce){
    if(ce.source == generator){
         try{
             scoreLock.lock();
             if(char2type != -1){
                  score--;
                  setScore();
             }
             char2type = ce.character;
         }finally{
             scoreLock.unlock();
         }
     }
     else{
         try{
             scoreLock.lock();
             if(char2type != ce.character){
                  score--;
             }
             else{
                 score++;
                 char2type = -1;
             }
             setScore();
         }finally{
              scoreLock.unlock();
         }
     }

Lock這個接口有兩個方法:lock()和unlock(),咱們能夠在開始的時候調用lock(),而後在結束的時候調用unlock(),這樣就能有效的同步化這個方法。
咱們能夠看到,其實使用Lock接口只是爲了讓Lock更加容易被管理:咱們能夠存儲,傳遞,甚至是拋棄,其他和使用synchronized是同樣的,但更加靈活:咱們能夠在有須要的時候才獲取和釋放鎖,由於lock再也不依附於任何調用方法的對象,咱們甚至可讓兩個對象共享同一個lock!也可讓一個對象佔有多個lock!!

使用Lock接口,是一種明確的加鎖機制,以前咱們的加鎖是咱們沒法掌握的,咱們沒法知道是哪一個線程的哪一個方法得到鎖,但能確保同一時間只有一個線程的一個方法得到鎖,如今咱們能夠明確得的把握這個過程,靈活的設置lock scope,將一些耗時和具備線程安全性的代碼移出lock scope,這樣咱們就能夠寫出高效並且線程安全的程序代碼,不用像以前同樣,爲了防止未知錯誤必須對全部相關方法進行同步。

使用lock接口,能夠方便的利用它裏面提供的一些便利的方法,像是tryLock(),它能夠嘗試取得鎖,若是沒法獲取,咱們就能夠執行其餘操做,而不是浪費時間在等待鎖的釋放。tryLock()還能夠指定等待鎖的時間。

synchronized不只能夠同步方法,它還能夠同步一個程序塊:

public void newCharacter(CharacterEvent ce){
     if(ce.source == generator){
           synchronized(this)[
                  if(char2type != -1){
                        score--;
                        setScore();
                  }
                  char2type = ce.character;
            }
      }
      else{
            synchronized(this){
                  if(char2type != ce.character){
                        score--;
                  }
                  else{
                        score--;
                        char2type = -1;
                   }
                   setScore();
            }
      }
}

若是是爲了縮小lock的範圍,咱們依然仍是可使用synchronized而不是使用lock接口,並且這種方式纔是更加常見的,由於使用lock接口時咱們須要建立新的對象,須要異常管理。咱們能夠lock住其餘對象,如被共享的數據對象。
選擇synchronized整個方法仍是代碼塊,都沒有什麼問題,但lock scope仍是儘量的越小越好。

考慮到newCharacter()這個方法裏面出現了策略選擇,咱們能夠對它進行重構:

private synchronized void newGeneratorCharacter(int c){
      if(char2type != -1){
            score--;
            setScore();
      }
      char2type = c;
}

private synchronized void newTpistCharacter(int c){
      if(char2type != c){
             score--;
      }
      else{
           score++;
           char2type = -1;
      }
      setScore();
}

public synchronized void newCharacter(CharacterEvent ce){
       if(ce.source == generator){
              newGeneratorCharacter(ce.character);
       }
       else{
            newTypistCharacter(ce.character);
       }
}

咱們會注意到,兩種策略方法都要用synchronized鎖住,但真的有必要嗎?由於它們是private,只會在該對象中使用,沒有理由要讓這些方法獲取鎖,由於它們也只會被對象內的synchronized方法調用,而這時已經得到鎖了。可是咱們仍是要這樣作,考慮到之後的開發者可能不知道調用這些方法以前須要獲取鎖的狀況。
因而可知,java的鎖機制遠比咱們想象中要聰明:它並非盲目的在進入synchronized程序代碼塊時就開始獲取鎖,若是當前的線程已經得到鎖,根本就沒有必要等到鎖被釋放仍是去獲取,只要讓synchronized程序段運行就能夠。若是沒有獲取鎖,也就不會將它釋放掉。這種機制之因此可以運行是由於系統會保持追蹤遞歸取得lock的數目,最後會在第一個取得lock的方法或者代碼塊退出的時候釋放鎖。

這就是所謂的nested lock。

以前咱們使用的ReentrantLock一樣支持nested lock:若是lock的請求是由當前佔有lock的線程發出,內部的nested lock就會要求計數遞增,調用unlock()就會遞減,直到計數爲0就會釋放該鎖。但這個是ReentrantLock才具備的特性,其餘實現了Lock這個接口的類並不具備。

nested lock是很是重要的,由於它有利於避免死鎖的發生。死鎖的發生遠比咱們想象中要更常見,像是方法間的相互調用,更加常見的狀況就是回調,像是Swing編程中依賴事件處理程序與監聽者的窗口系統,考慮一下監聽者常常變更的狀況,同步簡直就是一個惡夢!!

Synchronized沒法知道lock被遞歸調用的次數,可是使用ReentrantLock能夠作到這點。咱們能夠經過getHoldCount()方法來得到當前線程對lock所要求的數量,若是數量爲0,表明當前線程並未持有鎖,可是還不能知道鎖是自由的,咱們必須經過isLocked()來判斷。咱們還能夠經過isHeldByCurrentThread()來判斷lock是否由當前的線程所持有,getQueueLength()能夠用來取得有多少個線程在等待取得該鎖,但這個只是預估值。

在多線程編程中常常講到死鎖,可是即便沒有涉及到同步也有可能會產生死鎖。死鎖之因此是個問題,是由於它會讓程序沒法正確的執行,更加可怕的是,死鎖是很難被檢測的,特別是多線程編程每每都會是一個複雜的程序,它可能永遠也不會被發現!!

更加悲哀的是,系統沒法解決死鎖這種狀況!

最後一個問題是關於公平的授予鎖。

咱們知道,鎖是要被授予線程的,可是應該按照什麼依據來授予呢?是按照先到先得嗎?仍是服務請求最多?或者是對系統最有利的形式來授予?java的同步行爲最接近第三種,由於同步並非用來對特殊狀況授予鎖,它是通用的,因此沒有理由讓鎖按照到達的順序來授予,應該是由各實現所定義在底層線程系統的行爲所決定,但ReentrantLock提供了一種選項能夠按照先進先出的順序獲取鎖:new ReentrantLock(true),這是爲了防止發生鎖飢餓的現象。

咱們能夠根據本身的具體實現來決定這種公平。

最後,咱們來總結一下:

1.對於同時涉及到靜態和非靜態方法的同步狀況,使用lock對象更加容易,由於lock對象無關於使用它的對象。

2.將整個方法同步化是最簡單的,可是這樣範圍會變大,讓確實沒有必要的程序段無效率的持有鎖。

3.若是涉及到太多的對象,使用同步塊機制也是有問題的,同步塊沒法解決跨方法的鎖範圍。

(五)等待與通知機制

在以前咱們關於中止Thread的討論中,曾經使用過設定標記done的作法,一旦done設置爲true,線程就會結束,一旦爲false,線程就會永遠運行下去。這樣作法會消耗掉許多CPU循環,是一種對內存不友好的行爲。

java中的對象不只擁有鎖,並且它們自己就能夠經過調用相關方法使本身成爲等待者和通知者。

Object對象自己有兩個方法:wait()和notify()。wait()會等待條件的發生,而notify()會通知正在等待的線程此條件已經發生,它們都必須從synchronized方法或塊中調用。

這種等待-通知機制的目的到底是爲什麼?

等待-通知機制是一種同步機制,但它更像是一個通訊機制,可以讓一個線程與另外一個線程在某個特定條件下進行通訊。可是,該機制卻沒有指定特定條件是什麼。

等待-通知機制可否取代synchronized機制嗎?固然不行,等待-通知機制並不會解決synchronized機制可以解決的競爭問題,實際上,這二者是相互配合使用的,並且它自己也存在競爭問題,這是須要經過synchronzied來解決的。

private boolean done = true;

public synchronized void run(){
      while(true){
            try{
                 if(done){
                       wait();
                 }else{
                       repaint();
                       wait(100);
                 }
            }catch(InterruptedException e){
                  return;
            }
      }
}

public synchronized void setDone(boolean b){
     done = b;
     if(timer == null){
          timer = new Thread(this);
          timer.start();
     }
     if(!done){
          notify();
     }
}

這裏的done已經不是volatile,由於咱們不僅是設定個標記值,咱們還須要在設定標記的同時自動發送一個通知。因此,咱們如今是經過synchronized來保護對done的訪問。
run()方法不會在done爲false時自動退出,它會經過調用wait()方法讓線程在這個方法中等待,直到其餘線程調用notify()方法。

這裏有幾個地方值得咱們注意。

首先,咱們這裏經過使用wait()方法而不是sleep()方法來使線程休眠,由於wait()方法須要線程持有該對象的同步鎖,當wait()方法執行的時候,該鎖就會被釋放,而當收到通知的時候,線程須要在wait()方法返回前從新得到該鎖,就好像一直都持有鎖同樣。這個技巧是由於在設定與發送通知以及測試與取得通知之間是存在競爭的,若是wait()和notify()在持有同步鎖的同時沒有被調用,是徹底沒有辦法保證此通知會被接收到的,而且若是wait()方法在等待前沒有釋放掉鎖,是不可能讓notify()方法被調用到,由於它沒法取得鎖,這也是咱們之因此使用wait()而不是sleep()的另外一個緣由。若是使用sleep()方法,此鎖就永遠不會被釋放,setDone()方法也永遠不會執行,通知也永遠不會送出。

接着就是這裏咱們對run()進行同步化。咱們以前討論過,對run()進行同步是很是危險的,由於run()方法是絕對不可能會完成的,也就是鎖永遠不會被釋放,可是由於wait()自己就會釋放掉鎖,因此這個問題也被避免了。

咱們會有一個疑問:若是在notify()方法被調用的時候,沒有線程在等待呢?

等待-通知機制並不知道所送出通知的條件,它會假設通知在沒有線程等待的時候是沒有被收到的,由於這時它也只是返回且通知也被遺失掉,稍後執行wait()方法的線程就必須等待另外一個通知。

上面咱們講過,等待-通知機制自己也存在競爭問題,這真是一個諷刺:本來用來解決同步問題的機制自己居然也存在同步問題!其實,競爭並不必定是個問題,只要它不引起問題就行。咱們如今就來分析一下這裏的競爭問題:

使用wait()的線程會確認條件不存在,這一般是經過檢查變量實現的,而後咱們才調用wait()方法。當其餘線程設立了該條件,一般也是經過設定同一個變量,纔會調用notify()方法。競爭是發生在下列幾種狀況:

1.第一個線程測試條件並確認它須要等待;

2.第二個線程設定此條件;

3.第二個線程調用notify()方法,這並不會被收到,由於第一個線程尚未進入等待;

4.第一個線程調用wait()方法。

這種競爭就須要同步鎖來實現。咱們必須取得鎖以確保條件的檢查和設定都是automic,也就是說檢查和設定都必須處於鎖的範圍內。

既然咱們上面講到,wait()方法會釋放鎖而後從新獲取鎖,那麼是否會有競爭是發生在這段期間呢?理論上是會有,但系統會阻止這種狀況。wait()方法與鎖機制是緊密結合的,在等待的線程尚未進入準備好能夠接收通知的狀態前,對象的鎖其實是不會被釋放的。

咱們的疑問還在繼續:線程收到通知,是否就能保證條件被正確的設定呢?抱歉,答案不是。在調用wait()方法前,線程永遠應該在持有同步鎖時測試條件,在從wait()方法返回時,該線程永遠應該從新測試條件以判斷是否還須要等待,這是由於其餘的線程一樣也可以測試條件並判斷出無需等待,而後處理由發出通知的線程所設定的有效數據。但這是在只有一個線程在等待通知,若是是多個線程在等待通知,就會發生競爭,並且這是等待-通知機制所沒法解決的,由於它能解決的只是內部的競爭以防止通知的遺失。多線程等待最大的問題就是,當一個線程在其餘線程收到通知後再收到通知,它沒法保證這個通知是有效的,因此等待的線程必須提供選項以供檢查狀態,並在通知已經被處理的情形下返回到等待的狀態,這也是咱們爲何老是要將wait()放在循環裏面的緣由。

wait()也會在它的線程被中斷時提早返回,咱們的程序也必需要處理該中斷。

在多線程通知中,咱們如何確保正確的線程收到通知呢?答案是不行的,由於咱們根本就沒法保證哪個線程可以收到通知,可以作到的方法就是全部等待的線程都會收到通知,這是經過notifyAll()實現的,但也不是真正的喚醒全部等待的線程,由於鎖的問題,實質上全部的線程都會被喚醒,可是真正在執行的線程只有一個。

之因此要這樣作,多是由於有一個以上的條件要等待,既然咱們沒法確保哪個線程會被喚醒,那就乾脆喚醒全部線程,而後由它們本身根據條件判斷是否要執行。

等待-通知機制能夠和synchronized結合使用:

private Object doneLock = new Object();

public void run(){
     synchronized(doneLock){
           while(true){
                if(done){
                      doneLock.wait();
                }else{
                      repaint();
                      doneLock.wait(100);
                }
           }catch(InterruptedException e){
                 return;
           }
     }
}

public void setDone(boolean b){
     synchronized(doneLock){
          done = b;
          if(timer == null){
               timer = new Thread(this);
               timer.start();
          }
          if(!done){
                doneLock.notify();
          }
     }
}

這個技巧是很是有用的,尤爲是在具備許多對對象鎖的競爭中,由於它可以在同一時間內讓更多的線程去訪問不一樣的方法。
最後咱們要介紹的是條件變量。

J2SE5.0提供了Condition接口。Condition接口是綁定在Lock接口上的,就像等待-通知機制是綁定在同步鎖上同樣。

private Lock lock = new ReentrantLock();
private Condition  cv = lockvar.newCondition();

public void run(){
     try{
          lock.lock();
          while(true){
               try{
                   if(done){
                         cv.await();
                   }else{
                         nextCharacter();
                         cv.await(getPauseTime(), TimeUnit.MILLISECONDS);
                   }
               }catch(InterruptedException e){
                     return;
               }
          }
     }finally{
           lock.unlock();
     }
}

public void setDone(boolean b){
    try{
         lock.lock();
         done = b;
         if(!done){
               cv.signal();
         }finally{
               lock.unlock();
         }
    }
}

上面的例子好像是在使用另外一種方式來完成咱們以前的等待-通知機制,實際上使用條件變量是有幾個理由的:
1.條件變量在使用Lock對象時是必須的,由於Lock對象的wait()和notify()是沒法運做的,由於這些方法已經在內部被用來實現Lock對象,更重要的是,持有Lock對象並不表示持有該對象的同步鎖,由於Lock對象和對象所關聯的同步鎖是不一樣的。

2.Condition對象不像java的等待-通知機制,它是被建立成不一樣的對象,對每一個Lock對象均可以建立一個以上的Condition對象,因而咱們能夠針對個別的線程或者一羣線程進行獨立的設定,也就是說,對同一個對象上全部被同步化的在等待的線程都得等待相同的條件。

基本上,Condition接口的方法都是複製等待-通知機制,可是提供了避免被中斷或者能以相對或絕對時間來指定時限的便利。

(六)Atomic變量和Thread局部變量

前面咱們已經講過如何讓對象具備Thread安全性,讓它們可以在同一時間在兩個或以上的Thread中使用。Thread的安全性在多線程設計中很是重要,由於race condition是很是難以重現和修正的,咱們很難發現,更加難以改正,除非將這個代碼的設計推翻來過。

同步最大的問題不是咱們在須要同步的地方沒有使用同步,而是在不須要同步的地方使用了同步,致使效率極度低下。因此,咱們要想辦法限制同步,由於無謂的同步比起無謂的運算還更加讓人無語。

可是否有辦法徹底避免同步呢?

在有些狀況下是能夠的。咱們可使用以前的volatile關鍵字來解決這個問題,由於volatile修飾的變量是被完整的存儲的,在讀取它們的時候,可以確保它們是有效的,也就是最近一次存入的值。但這也是能夠避免同步的惟一狀況,若是有多個線程同時訪問同一份數據,就必須明確的同步化全部對該數據的訪問以防止各類race condition。

爲何沒法徹底避免呢?

每組線程都有本身的一組寄存器,但系統將某個線程分配給CPU時,它會把該線程持有的信息加載到CPU的寄存器中,在分配不一樣的線程給CPU前,它會將寄存器的信息保存下來,因此線程之間毫不會共享保存在寄存器中的數值,可是經過使用volatile,咱們能夠確保變量不會保持在寄存器中,這點咱們在以前的文章中已經說過了,這就可以確保變量是真正的共享於線程之間。可是同步爲何可以解決這個問題呢?由於當虛擬機進入synchronized方法或者synchronized塊的時候,它必須從新加載本來已經緩衝到自有寄存器上的數據,也就是存入到主存儲器中。

也就是說,除了使用volatile和同步,咱們就沒有方法保證被線程共享的數據在訪問上的安全性,但事實證實,volatile並非值得推薦的解決方法,因此也只剩下同步了。

既然這樣,咱們惟一可以作到的就是學會恰當的使用同步。

同步的目的就是防止race condition致使數據在不一致或者變更中間狀態被使用到,這段期間會禁止線程間的競爭。但這個保證會由於一個微妙的問題而變得不可信:線程間可能在同步的程序代碼運行前就開始競爭。

並非全部的race condition都應該避免,只有在無Thread安全性的程序段中的race condition纔會被認爲是問題。咱們可使用兩種方法來解決這個問題:使用synchronized程序代碼來防止race condition的發生或者將程序設計成無需同步(或僅使用最少的同步)就具備Thread安全性。

對於第二種方法,咱們應該儘量縮小同步的範圍並從新組織程序代碼以便讓具備Thread安全性的段落可以被移出synchronized塊以外,這點很是重要,若是有足夠的程序代碼可以被移出synchronized塊以外,咱們甚至根本就不須要進行同步。

咱們可使用volatile來減小同步,可是volatile只能針對單一的載入或者存儲操做,但不少狀況下都不是這樣子,因此它的使用是比較不常見的。

J2SE 5.0提供了一組atomic class來處理更加複雜的狀況。相對於只能處理單一的atomic操做,這些class可以讓多個操做被atomic地對待,這樣咱們也就有可能不須要同步就能實現同步機制所作到的一切。

咱們能夠用AtomicInteger,AtomicLong,AtomicBoolean和AtomicReference這四個class實現的四個基本的atomic類型來處理integer,long,boolean和對象。這些class都提供了兩個構造器:默認的構造器的值爲0,false,false或者null,另外一個構造器是以程序設計者所指定的值來初始化和建立變量。set()和get()這兩個方法就提供了volatile變量所具備的的功能:可以atomic的設定與取得值,由於它們可以確保數據的讀寫是從主存儲器上運行的。可是這些class還提供了更多的操做來適應volatile沒法解決的狀況。

getAndSet()可以在返回初始值的時候atomic的設定變量成新值,徹底不須要任何的同步lock。compareAndSet()與weakCompareAndSet()是有條件的修改程序的方法,這兩個方法都要取用兩個參數:在方法啓動時預期數據所具備的的值,以及要把數據所設定成的值。它們都只會在變量具備預期值的時候纔會設定成新值,若是當前值不等於預期值,該變量就不會被從新賦值而且返回false。這兩個方法之間有什麼區別嗎?第二個方法少了一項保證:若是方法返回的值false,該變量不會被變更,可是這並不表示現有值不是預期值,也就是說,這個方法無論初始值是不是預期值均可能會沒法更新改值。

incrementAndGet(),decrementAndGet(),getAndIncrement()和getAndDecrement()提供了前置遞增,前置遞減,後遞增和後遞減,之因此有這些方法,是由於這些操做都不是atomic的。

addAndGet()和getAndAdd()提供了"前置"和"後置"的運算符給指定值的加法運算,它們可以讓程序對變量增或者減一個指定值,包括了負值,因此咱們就不須要一個相對的減法運算。

atomic package目前沒有實現atomic字符或者浮點變量,可是咱們可使用AtomicInteger來處理字符,就像是字符常量同樣,可是使用atomic的浮點數須要atomic帶有隻讀浮點數值的受管理對象。咱們也沒有實現atomic數組,並無功能可以對整個數組作atomic化的變更,最多就是經過使用AtomicInteger,AtomicLong和AtomicReference的數組來模型化,可是數組的大小必須在構造時就指定好,而且在操做過程當中必須提供索引。至於Boolean的atomic數組,一樣也能夠經過AtomicInteger來實現。

atomic package還有兩個類:AtomicMarkableReference和AtomicStampedReterence。這兩個類可以讓mark或stamp跟在任何對象的引用上,更精確點講,AtomicMarkableReference提供了一種包括對象引用結合boolean的數據結構,而AtomicStampedReference提供了一種包括對象引用結合integer的數據結構。

其實,atomic class的這些方法在本質上是同樣的。在使用的時候,get()方法須要咱們傳入一個數組做爲參數,stamp或者mark被存儲在數組的第一個元素而引用被正常返回。其餘的get()方法就只是返回引用,mark或者stamp。

set()與compareAndSet()方法須要額外的參數來表明mark或者stamp。最後,這些class都帶有attemptMark()或attemptStamp()方法,用來依據期待的引用設定mark或者stamp。

到了這裏,咱們也許會欣喜的將每一個程序或者class改爲只用atomic變量,事實上,這種嘗試並不僅是替換變量那麼簡單。atomic class並非同步工具的直接替代品,它們的使用會讓咱們的程序設計更加複雜,就算只是一些簡單的class也是這樣。

咱們來舉一個例子:

public class ScoreLabel extends JLabel implements CharacterListener {
    private volatile int score = 0;
    private int char2type = -1;
    private CharacterSource generator = null, typist = null;
    private Lock scoreLock = new ReentrantLock();

    public ScoreLabel(CharacterSource generator, CharacterSource typist) {
        this.generator = generator;
        this.typist = typist;
        if (generator != null) {
            generator.addCharacterListener(this);
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public ScoreLabel() {
        this(null, null);
    }

    public void resetGenerator(CharacterSource newCharactor) {
        try {
            scoreLock.lock();
            if (generator != null) {
                generator.removeCharacterListener(this);
            }
            generator = newCharactor;
            if (generator != null) {
                generator.addCharacterListener(this);
            }
        } finally {
            scoreLock.unlock();
        }
    }

    public void resetTypist(CharacterSource newTypist) {
        if (typist != null) {
            typist.removeCharacterListener(this);
            typist = newTypist;
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public synchronized void resetScore() {
        score = 0;
        char2type = -1;
        setScore();
    }

    private synchronized void setScore() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                setText(Integer.toString(score));
            }
        });
    }

    @Override
    public synchronized void newCharacter(CharacterEvent ce) {
        if (ce.source == generator) {
            if (char2type != -1) {
                score--;
                setScore();
            }
            char2type = ce.character;
        } else {
            if (char2type != ce.character) {
                score--;
            } else {
                score++;
                char2type = -1;
            }
            setScore();
        }
    }
}

爲了修改這個類,咱們須要三個修改:簡單的變量代換,算法的變動和從新嘗試操做,每個修改都要保持class的synchronized版本語義的完整,而這些都是依賴於程序代碼全部的效果,因此咱們必須確保程序代碼的最終效果和synchronized版本是一致的,這個目的也是重構的基本原則:在不影響代碼外在表現下對代碼進行內在的修改,也是面向對象的核心思想。

變量代換是最簡單的操做,咱們只要將以前所使用的變量替換成atomic變量。像是咱們這裏就能夠將resetScore()中的score和char2type這兩個變量修改爲atomic變量。但有意思的是,將這兩個變量一塊兒變動的動做並非atomic地完成,仍是有可能會讓char2type變量的變動在完成前就變動了score。這彷佛是個問題,但實際上並非這樣,由於咱們還保持住這個類在synchronized版本上的語義。要記住,同步的目的是爲了消除有問題的race condition,有一些race condition根本就不是問題。咱們再來舉個例子:resetScore()和newCharacter()方法都是synchronized,但這也只是意味着二者不會同時運行,被拖延住的newCharacter()方法的調用仍是可能會由於到達的順序或者取得lock的順序而延遲運行,因此打字輸入的事件可能會等到resetScore()方法完成後纔會被傳遞,但這時傳遞到的只是個已通過時的事件,這些也是出於一樣的緣由:在resetScore()方法中同時變動兩個變量這個動做並無被atomic地處理。

第二個修改是變動算法。

咱們來看看resetGenerator()和resetTypist()這兩個方法的新的實現。以前咱們對這兩個方法所作的,就是嘗試將二者的同步lock分離。這的確是個不錯的主意,這兩個方法都沒有變更score或者char2type變量,事實上,它們甚至也沒有變更到相互共享的變量,由於resetGenerator()方法和resetTypist()的同步lock只是用來保護此方法不受多個Thread同時地調用。可是,若是隻是簡單的將generator變量變成AtomicReference,那麼以前咱們解決過的問題都會從新引起。

之因此會是這樣,是由於resetGenerator()這個方法封裝的狀態並不僅是generator變量的值,讓generator變量變成AtomicReference表示咱們知道對該變量的操做是atomic地發生,但當咱們要從resetGenerator()方法中徹底的刪除掉同步時,咱們還必須確保整個由此方法所封裝住的狀態仍是一致的,而這些所謂的狀態,包括了在字母源產生器上ScoreLabel對象的登記(this對象),在這個方法完成後,咱們要確保this對象只有被登記過一次到惟一一個產生器上,也就是被分配到generator的instance變量上的那一個。

想一下這個具體的情景:現有的產生器是generatorA,某個線程以generatorB產生器調用resetGenerator(),而另外一個線程以稱爲generatorC的產生器來調用此方法。

咱們以前的代碼是這樣的:

if (generator != null) {            
     generator.removeCharacterListener(this);
}
generator = newCharactor;
if (generator != null) {
     generator.addCharacterListener(this);
}

這段代碼最大的問題就是:兩個線程同時要求generatorA刪除this對象,實際上它會被刪除兩次,ScoreLabel對象一樣也會加入generatorB和generatorC。這兩個結果都是錯的。

可是咱們前面使用了synchronized來防止這樣的錯誤,但若是是沒有使用synchronized的前提下:

if (newGenerator != null) {
     newGenerator.addCharacterListener(this);
}
oldGenerator = generator.getAndSet(newGenerator);
if (oldGenerator != null) {
     oldGenerator.removeCharacterListener(this);
}

當它被兩個線程同時調用時,ScoreLabel對象會被generatorB和generatorC登記,各個線程隨後會atomic地設定當前的產生器,由於它們是同時運行,可能會有不一樣的結果:假設第一個線程先運行,它會從getAndSet()中取回generatorA,而後將ScoreLabel對象從generatorA的監聽器中刪除,而第二個線程從getAndSet()中取回generatorB並從generatorB的監聽器刪除ScoreLabel。若是第二個線程先運行,變量會稍有不一樣,但結果永遠會是同樣的:無論哪個對象被分配給genrator的instance變量,它就是ScoreLabel對象所監聽的那一個,而且是惟一的一個。

可是這裏會有一個反作用:交換以後監聽器會從舊的數據來源中被刪除掉,且監聽器會在交換前被加入到新的數據來源,它如今有可能接收到既不是現有的產生器也不是打字輸入來源所產出的字符。以前newCharacter()方法會檢查來源是否爲產生器的來源,並在不是的時候會假設來源是打字輸入來源。如今就再也不是這樣,newCharacter()方法必須在處理它以前確認字母的來源,它也必須忽略掉來自不正確的監聽器的字母。

因此咱們的最後一步就是從新嘗試操做:

@Override
    public synchronized void newCharacter(CharacterEvent ce) {
        int oldChar2type;
        if (ce.source == generator.get()) {
            oldChar2type = char2type.getAndSet(ce.character);
            if (oldChar2type != -1) {
                score.decrementAndGet();
                setScore();
            }
        } else if (ce.source == typist.get()) {
            while (true) {
                oldChar2type = char2type.get();
                if (oldChar2type != ce.character) {
                    score.decrementAndGet();
                    break;
                } else if (char2type.compareAndSet(oldChar2type, -1)) {
                    score.incrementAndGet();
                    break;
                }
            }
            setScore();
        }

newCharacter()這個方法的修改是最大的,由於它如今必需要丟棄任何不是來自於所屬來源的事件。

重點是打字輸入事件的處理。咱們須要檢查輸入的字母是不是正確的,若是不是,玩家就會被懲罰,這是經過atomic地遞減分數來完成的。若是字母被正確的輸入,玩家沒法被當即的給予獎賞,相對的,char2type變量要先被更新,分數只有在char2type被正確更新時纔會更新。若是更新的操做失敗了,這表明着在咱們處理此事件時,有其餘事件已經被其餘線程處理過了,而且是成功處理完。

什麼叫成功的處理完?它表示咱們必須從新處理事件,由於咱們是基於這樣的假設:假設正在使用的該變量值不會被變動而且程序代碼完成時也是這樣,全部已經被咱們設定爲具備特定值的變量就確實應該是那個值,但由於這與其餘線程衝突,這些假設也就被破壞了。經過從新嘗試處理事件,就好像從未遇到衝突同樣。

因此咱們才須要將這段代碼封裝在一個無窮循環裏:程序不會離開循環直到事件被成功處理掉。顯然在多個事件間存在race condition,循環會確保沒有一個事件會被漏掉或者處理了超過一次。只要咱們確實只處理有效的事件一次,事件被處理的順序就不重要了,由於在處理完每一個事件後,數據就會保持在無缺的狀態。實際上,當咱們使用同步的時候,也是同樣的情形:多個事件並無以指定的順序進行,它們只是根據授予lock的順序來進行。

整個代碼如:

public class ScoreLabel extends JLabel implements CharacterListener {
    private AtomicInteger score = new AtomicInteger(0);
    private AtomicInteger char2type = new AtomicInteger(-1);
    private AtomicReference<CharacterSource> generator = null;
    private AtomicReference<CharacterSource> typist = null;

    public ScoreLabel(CharacterSource generator, CharacterSource typist) {
        this.generator = new AtomicReference<CharacterSource>();
        this.typist = new AtomicReference<CharacterSource>();

        if (generator != null) {
            generator.addCharacterListener(this);
        }
        if (typist != null) {
            typist.addCharacterListener(this);
        }
    }

    public ScoreLabel() {
        this(null, null);
    }

    public void resetGenerator(CharacterSource newGenerator) {
        CharacterSource oldGenerator;
        if (newGenerator != null) {
            newGenerator.addCharacterListener(this);
        }
        oldGenerator = generator.getAndSet(newGenerator);
        if (oldGenerator != null) {
            oldGenerator.removeCharacterListener(this);
        }
    }

    public void resetTypist(CharacterSource newTypist) {
        CharacterSource oldTypist;
        if (newTypist != null) {
            newTypist.addCharacterListener(this);
        }
        oldTypist = typist.getAndSet(newTypist);
        if (oldTypist != null) {
            oldTypist.removeCharacterListener(this);
        }
    }

    public synchronized void resetScore() {
        score.set(0);
        char2type.set(-1);
        setScore();
    }

    private synchronized void setScore() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                setText(Integer.toString(score.get()));
            }
        });
    }

    @Override
    public synchronized void newCharacter(CharacterEvent ce) {
        int oldChar2type;
        if (ce.source == generator.get()) {
            oldChar2type = char2type.getAndSet(ce.character);
            if (oldChar2type != -1) {
                score.decrementAndGet();
                setScore();
            }
        } else if (ce.source == typist.get()) {
            while (true) {
                oldChar2type = char2type.get();
                if (oldChar2type != ce.character) {
                    score.decrementAndGet();
                    break;
                } else if (char2type.compareAndSet(oldChar2type, -1)) {
                    score.incrementAndGet();
                    break;
                }
            }
            setScore();
        }
    }
}

atomic變量的目的只是避免同步的性能因素,可是爲何它在無窮循環中的時候還比較快呢?從技術上來說,答案確定不是那個無窮循環,額外的循環只會發生在atomic操做失敗的時候,這是由於與其餘線程發生衝突。要發生一個真正的無窮循環,就須要無窮的衝突。若是使用同步,這也會是一個問題:無數的線程要訪問lock一樣也會讓程序沒法正常操做。另外一個方面,實際上,atomic class與同步間的性能差別一般不是很大。

因此,咱們有必要平衡同步和atomic變量的使用。在使用同步的時候,線程在取得lock前 會被block住而不能執行。這可以讓程序由於其餘的線程被阻擋不能執行而atomic地來運行。當使用atomic變量的時候,線程是可以並行的運行相同的代碼,atomic變量的目的不是消除不具備線程安全性的race condition,它們的目的是要讓程序代碼具備線程安全性,因此就不用特意去防止race condition。

使用atomic變量正是應了這樣的一句老話:天下沒有白吃的午飯!咱們避開了同步,可是在所運行的工做量上卻付出了代價。這就是所謂的樂觀同步:咱們的程序代碼抓住保護變量的值並做出在這瞬間沒有其餘修改的假設,而後程序代碼就計算出該變量的新值並嘗試更新該變量。若是有其餘線程同時修改了這個變量,這個更新就失敗而且程序必須從新執行這些步驟,而且使用變量的最新修改過的值。

上面例子中出現了數據交換,也就是在取得舊值的同時atomic地設定新值的能力。這是經過使用getAndSet()方法來完成的,使用這個方法就能確保只有一個線程可以取得並使用該值。

若是有更復雜的數據交換時該如何處理?若是值的設定要依據該舊值又該如何處理?

咱們能夠經過把get()和compareAndSet()方法放在循環中來處理。get()方法用來取得舊值,以計算新值,而後再經過使用compareAndSet()方法來設定新值----只有在舊值沒有被更動的時候纔會設定爲新值,若是這個方法失敗,整個操做能夠從新進行,由於當前的線程在失敗時都沒有動到任何數據。雖然調用過get()方法,計算過新值,數據的交換並非被個別地atomic,若是它成功,這個順序能夠被認爲是atomic的,由於它只在沒有其餘線程變更該值時纔會成功。

compareAndSet()這個方法處理的其實就是所謂比較與設定這種狀況。它是隻有在當前值是預期值的時候才atomic地設定值的能力,這個方法是在atmoic層提供條件支持能力的重要方法。,它甚至可以用來實現出由mutex所提供的同步能力。
若是是更加複雜的比較該如何處理?若是比較是要依據舊值或者外部值該如何處理?

咱們依然能夠經過把get()和compareAndSet()方法放在循環中來處理。由於數據交換和比較實際上是差很少的,惟一的區別就是get()方法取得舊值的目的是爲了用來比較或者只用來完成atomic地交換,而複雜的比較是能夠用來觀察是否要繼續操做。

雖然atomic class可用的數據類型列表數量是至關大的,但它依然不是完整的。它不能支持字符和浮點數據類型,雖然支持通常對象類型,可是沒有對更復雜的對象類型提供支持,像是String這類對象就具備不少方便的操做。可是,咱們是在JAVA中編程,也就是面向對象編程,咱們徹底能夠將數據類型封裝進只讀的數據對象中來對任何新類型實現atomic的支持,而後此數據對象經過改變atomic引用到新的數據對象,就能夠被atomic地變更。但這也僅僅在嵌入數據對象的值不會被任何方式變更的時候纔有效。任何對數據對象的變更必須只能經過改變引用到不一樣的對象來完成,舊對象的值是不變的。全部由數據對象所封裝的值,無論是直接仍是非直接,必須是隻讀的才能使這個技巧有效的運做。

因此,咱們是不可能atomic地改變浮點數值,可是咱們能夠atomic地改變對象引用到不一樣的浮點數值,只要浮點數值是隻讀的,它就具備線程安全性。

這就是咱們實現的浮點數值的atomic class:

public class AtomicDouble extends Number{
    private AtomicReference<Double> value;
    public AtomicDouble(){
        this(0.0);
    }
    
    public AtomicDouble(double initVal){
        value = new AtomicReference<Double>(new Double(initVal));
    }
    
    public double get(){
        return value.get().doubleValue();
    }
    
    public void set(double newVal){
        value.set(new Double(newVal));
    }

    public boolean compareAndSet(double expect, double update){
        Double origVal, newVal;
        
        newVal = new Double(update);
        while(true){
            origVal = value.get();
            
            if(Double.compare(origVal.doubleValue(), expect) == 0){
                if(value.compareAndSet(origVal, newVal)){
                    return true;
                }else{
                    return false;
                }
            }
        }
    }
    
    public boolean weakCompareAndSet(double expect, double update){
        return compareAndSet(expect, update);
    }
    
    public double getAndSet(double setVal){
        Double origVal, newVal;
        
        newVal = new Double(setVal);
        while(true){
            origVal = value.get();
            if(value.compareAndSet(origVal, newVal)){
                return origVal.doubleValue();
            }
        }
    }
    
    public double getAndAdd(double delta){
        Double origVal, newVal;
        
        while(true){
            origVal = value.get();
            newVal = new Double(origVal.doubleValue() + delta);
            if(value.compareAndSet(origVal, newVal)){
                return origVal.doubleValue();
            }
        }
    }
    
    public double addAndGet(double delta){
        Double origVal, newVal;
        
        while(true){
            origVal = value.get();
            newVal = new Double(origVal.doubleValue() + delta);
            if(value.compareAndSet(origVal, newVal)){
                return newVal.doubleValue();
            }
        }
    }
    
    public double getAndIncrement(){
        return getAndAdd((double)1.0);
    }
    
    public double getAndDecrement(){
        return addAndGet((double)-1.0);
    }
    
    public double incrementAndGet(){
        return addAndGet((double)1.0);
    }
    
    public double decrementAndGet(){
        return addAndGet((double)-1.0);
    }
    
    public double getAndMultiply(double multiple){
        Double origVal, newVal;
        
        while(true){
            origVal = value.get();
            newVal = new Double(origVal.doubleValue() * multiple);
            if(value.compareAndSet(origVal, newVal)){
                return origVal.doubleValue();
            }
        }
    }
    
    public double multiplyAndGet(double multiple){
        Double origVal, newVal;
        
        while(true){
            origVal = value.get();
            newVal = new Double(origVal.doubleValue() * multiple);
            if(value.compareAndSet(origVal, newVal)){
                return newVal.doubleValue();
            }
        }
    }
}

到如今爲止,咱們還只是對個別的變量作atomic地設定,尚未作到對一羣數據atomic地設定。若是是這樣,咱們就必須經過建立封裝這些要被變更值的對象來完成,以後這些值就能夠經過atomic地變更對這些值的atomic引用來作到同時地改變。這樣的運行方式其實和上面實現的AtomicDouble是同樣的。

這一樣也是須要在值沒有以任何方式直接改變的狀況下才會有效。任何對數據對象的改變是經過改變引用到不一樣的對象上來完成的,也就是所謂的額數據交換,舊的對象值必須沒有被變更過,無論是直接仍是間接封裝的值都必須是隻讀的才能讓這個技巧有效運做。

明白了這點,咱們也清楚的知道了以高級atomic數據類型來執行大量的數據變動,咱們會用到大量的臨時對象,由於爲了確保全部的操做是atomic的,咱們必須在臨時變量上來完成全部的計算,而且全部的值都是使用數據交換來作atomic地變動。實際上,對象的建立遠比咱們想象中要多得多:對每一個事務動做都須要建立一個新的對象,每一個atomic地比對和設定操做在失敗而必須從新嘗試的時候也須要建立新的對象。

因此,咱們必須明白一件事:使用atomic變量會讓咱們的代碼很是複雜,咱們必須在它與使用同步間取得平衡:是否能夠接受全部臨時對象的建立?此技巧是否比同步好?

咱們最後來說一下Thread的局部變量。

任何線程均可以在任意的時間定義該線程私有的局部變量,而其餘線程能夠定義相同的變量以建立該變量自有的拷貝。這就意味着線程的局部變量沒法用在線程間共享狀態,對某個線程私有的變量所作的改變並不會反映在其餘線程所持有的拷貝上,但這意味着對該變量的訪問覺不須要同步化,由於它不可能讓多個線程同時訪問。

咱們能夠利用java.lang.ThreadLocal這個類來模型化:

public class ThreadLocal<T>{
     protected T initialValue();
     public T get();
     public void set(T value);
     public void remove();
}

通常狀況下,咱們都是subclass這個ThreadLocal並覆寫initialValue()這個方法來返回應該在線程第一次訪問此變量時返回的值。咱們還能夠經過繼承自這個類來讓子線程繼承父線程的局部變量。

事實上,ThreadLocal由於性能很是差的緣由咱們不多使用到,可是它在實現一些線程問題的時候仍是很是有用的,像是Android的消息隊列,就是使用到了這點。

相關文章
相關標籤/搜索