【轉帖】Java Tutorials -- Concurrency(譯)

Java Tutorials -- Concurrency

    原文地址:http://www.blogjava.net/jiangshachina/archive/2007/10/28/156522.html

 

    近一段時間在使用Thinking in Java(4th, English)和Java Concurrency in Practice學習Java併發編程。不得不說官方的Java Tutorias是很好的Java併發編程入門級教程,故將它其中的Concurrency一章翻譯在了此處。與我翻譯Java Tutorias中Generics一章時的目的相同,只是對自己近一段時間學習的回顧罷了,也希望對其它朋友能有所助益。(2007.11.29最後更新)

課程: 併發
    計算機用戶們將他們的系統能夠在同一時刻做一件以上的事情視爲一種當然。他們猜想着,他們在使用字處理器的同時其它的應用程序正在下載文件,管理打印隊列和傳輸音頻流。例如,音頻流應用程序必須在同一時刻從網絡上讀取數字音頻數據,解壓它們,管理重放功能,並更新它們的顯示方式。
    Java平臺徹底地被設計爲支持併發的編程,它有着在Java程序設計語言和Java字節類庫中的基本併發支持。從5.0開始,Java平臺也將包含着高級的併發API。該課程介紹了該平臺的基本併發編程支持,並概述了java.util.concurrent包中的一些高級API。

進程與線程
    在併發編程中,有兩種基本的執行單元:進程和線程。在Java程序設計語言中,併發編程幾乎只關心線程。當然,進程也是重要的。
    計算機系統一般都有很多活躍的進程與線程。甚至在只有一個執行內核的系統中也是如此。在一個給定的時刻,確實只能有一個線程是在執行。通過一種稱之爲"分片"的操作系統特性,進程與線程共享着單核的處理時間。
    計算機系統擁有多個處理器或有多個執行內核的單處理器正在變得越來越普遍。這將很大地提升系統在處理進程與線程的併發執行時的能力。
進程
    一個進程擁有一個自我包括的執行環境。一個進程一般擁有一個完整的,內置的基本運行時資源;特別地,每個進程都擁有它自己的內存空間。
    進程經常被視爲程序或應用的同義詞。然而,用戶所看到的單個應用可能實際上是一組相互協作的進程。爲了便於進程之間的通信,大多數操作系統支持內部進程通信(Inter Process Communication, IPC),例如管道與套接字。IPC不僅被用於同一個系統中進程之間的通信,還可以處理不同系統中進程之間的通信。
    Java虛擬機的大多數實現都是作爲單個進程的。一個Java應用程序可以使用ProcessBuilder對象來創建額外的進程。多進程應用程序已經超出了本課程的範圍。
線程
    線程有時候被稱之爲輕量級進程。進程和線程都能提供一個執行環境,但創建一個線程所需要的資源少於創建一個進程。
    線程存在於一個進程中--每個進程至少有一個線程。線程分享進程的資源,包括內存和被打開的文件。這樣做是爲了高效,但在通信方面有着潛在的問題。
    多線程執行是Java平臺的一個本質特性。每個應用程序至少擁有一個線程--或者說是多個,如果你把像內存管理和信號處理這樣的系統進程算進來的話。但從應用程序員的角度來看,你僅以一個線程開始,該線程被稱之爲主線程。該線程擁有創建其它線程的能力,我們將在下一節中使用例子證明這一點。
線程對象
每個線程都關聯着一個Thread類的實例。爲了創建併發應用,有兩種使用Thread對象的基本策略。
* 爲了直接的控制線程的創建與管理,當應用每次需要啓動一個同步任務時就實例化一個Thread類。
* 從你的應用中將線程管理抽象出來,將應用的任務傳遞給一個執行器(Executor)。
本節將描述Thread對象的使用,執行器將與其它的高級併發對象們一起討論。
定義與啓動一個線程
一個應用要創建一個Thread的實例就必須提供那些要在該線程中運行的代碼。有兩種方法去做這些:
* 提供一個Runnable對象。Runnable接口只定義了一個方法--run,它要包含那些將在該線程中執行的代碼。Runnable對象將被傳入Thread的構造器,如例子HelloRunnable如示:
public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }
}
* 繼承Thread類。Thread類本身也實現了Runnable接口,可是它的run方法什麼都沒有做。一個應用可以繼承Thread類,並提供它自己的run方法實現,就如HelloThread類所做的:
public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new HelloThread()).start();
    }
}
    注意,爲了啓動新的線程,這兩個例子都調用了Thread.start方法。
    你應該使用哪一種方式呢?第一種方式使用了Runnable對象,這更加通用,因爲Runnable對象還可以繼承Thread之外的其它類。第二種方式可以很方便的使用在簡單應用中,但這樣的話,你的任務類必須被限制爲Thread的一個子類。本課程關注於第一種方式,該方法將Runnable任務從執行該任務的Thread對象中分離出來。這種方法不僅更富彈性,而且它還適用於後面將要提到的高級線程管理API中。
    Thread類爲線程的管理定義了一組很有有的方法。它們包括一些能夠提供關於該線程信息的靜態方法,以及當該線程調用這些方法時能夠影響到該線程的狀態。另一些方法則是被管理該線程和Thread對象的其它的線程調用。
使用sleep方法暫停執行
    Thread.sleep方法使當前運行的線程暫停執行一段指定的時間週期。這是使相同應用中的其它線程,或是相同計算機系統中的其它應用能夠獲得處理器時間的有效方法。sleep方法也被用於"緩步",如接下來的例子所示,並且等待其它負有任務的線程,這些線程被認爲有時間需求。
    sleep方法有兩個相互重載的版本:一個指定了睡眠時間的毫秒數;另一個指定了睡眠的納秒數。然而,這些睡眠時間都無法得到精確的保證,因爲受底層操作系統所提供的機制的限制。而且,睡眠週期會由於中斷而被停止,我們將在下面的章節中看到。在任何情況下,你都不能猜想調用sleep方法後都能精確地在指定時期週期內暫停線程。
    示例SeelpMessages使用sleep方法在每4秒的間隔內打印信息:
    public class SleepMessages {
        public static void main(String args[]) throws InterruptedException {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };

            for (int i = 0; i < importantInfo.length; i++) {
                //Pause for 4 seconds
                Thread.sleep(4000);
                //Print a message
                System.out.println(importantInfo[i]);
            }
        }
    }
    注意main方法聲明瞭它會拋出InterruptedException異常。當其它的線程中斷了正處於睡眠的當前線程時,sleep方法就會拋出該異常。由於該應用並沒有定義其它的線程去造成中斷,所以它沒必要去捕獲InterruptedException。
中斷
    中斷指明瞭一個線程應該停止它正在做的事情並做些其它的事情。它使程序員要仔細地考慮讓一個線程如何迴應中斷,但十分普通的做法是讓這個線程停終止。這是本課程特別強調的用法。
    一個線程以調用Thread對象中的interrupt方法的方式將中斷信號傳遞給另一個線程而使它中斷。爲了使中斷機制能正確地工作,被中斷的線程必須支持它自己的中斷。
支持中斷
    一個線程如何支持它自己的中斷呢?這取決於它當前正在幹什麼?如果該線程正在調用那些會拋出InterruptedException的方法,那麼當它捕獲了該異常之後就只會從run方法內返回。例如,假設SleepMessages示例中打印信息的循環就在該線程的Runnable對象的run方法中。再做如下修改,使它能夠中斷:
    for (int i = 0; i < importantInfo.length; i++) {
        //Pause for 4 seconds
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            //We've been interrupted: no more messages.
            return;
        }
            //Print a message
            System.out.println(importantInfo[i]);
    }
    許多會拋出InterruptedException異常的方法,如sleep,都被設計成當它們收到一箇中斷時就取消它們當前的操作並立即返回。
    如果該線程長時間運行且沒有調用會拋出InterruptedException異常的方法,那又會怎樣呢?它必須週期性地調用Thread.interrupted方法,如果收到了一箇中斷,該方法將返回true。例如:
    for (int i = 0; i < inputs.length; i++) {
        heavyCrunch(inputs[i]);
        if (Thread.interrupted()) {
            //We've been interrupted: no more crunching.
            return;
        }
    }
    在這個簡單的例子中,這些代碼僅是簡單地測試該線程是否收到了中斷,如果收到了就退出。在更復雜的例子中,它可能爲了更有意義些而拋出一個InterruptedException異常:
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
這就能使處理中斷的代碼被集中在catch語句塊中。
中斷狀態標誌
    被認爲是中斷狀態的內部標誌用於中斷機制的實現。調用Thread.interrupt方法會設置這個標記。當一個線程調用靜態方法Thread.interrupted去檢查中斷時,中斷狀態就被清理了。用於一個線程查詢另一個線程中斷狀態的非靜態方法Thread.isInterrupted不會改變中斷狀態標誌。
    按照慣例,任何通過拋出InterruptedException異常而退出的方法都會在它退出時清理中斷狀態。然而,總存在着這樣的可能性,中斷狀態會由於其它線程調用interrupt方法而立即被再次設置。
Joins
    join允許一個線程等待另一個線程完成。如果Thread對象t的線程當前正在執行,
    t.join();
上述語句將導致當前線程暫停執行只到t的線程終止爲止。join的一個重載版本允許程序員指定等待的週期。然而,與sleep方法一樣,join的時長依賴於操作系統,所以你不應該設想join將準確地等待你所指定的時長。
    像sleep方法一樣,在由於InterruptedException異常而退出時,join方法也要應對中斷。
SimpleThreads示例
    下面的例子彙集了本節一些概念。SimpleThreads類由兩個線程組成。第一個線程就是每個Java應用都有的主線程。主線程從一個Runnable對象,MessageLoop,創建一個新的線程,並等待它結束。如果MessageLoop線程花的時間太長了,主線程就會中斷它。
    MessageLoop線程打印出一系列的信息。如果在打印出所以信息之前就被中斷了,MessageLoop線程將會打印一條信息並退出。
    public class SimpleThreads {

        //Display a message, preceded by the name of the current thread
        static void threadMessage(String message) {
            String threadName = Thread.currentThread().getName();
            System.out.format("%s: %s%n", threadName, message);
        }

        private static class MessageLoop implements Runnable {
            public void run() {
                String importantInfo[] = {
                    "Mares eat oats",
                    "Does eat oats",
                    "Little lambs eat ivy",
                    "A kid will eat ivy too"
                };
                try {
                    for (int i = 0; i < importantInfo.length; i++) {
                        //Pause for 4 seconds
                        Thread.sleep(4000);
                        //Print a message
                        threadMessage(importantInfo[i]);
                    }
                } catch (InterruptedException e) {
                    threadMessage("I wasn't done!");
                }
            }
        }

        public static void main(String args[]) throws InterruptedException {
            //Delay, in milliseconds before we interrupt MessageLoop
            //thread (default one hour).
            long patience = 1000 * 60 * 60;

            //If command line argument present, gives patience in seconds.
            if (args.length > 0) {
                try {
                    patience = Long.parseLong(args[0]) * 1000;
                } catch (NumberFormatException e) {
                    System.err.println("Argument must be an integer.");
                    System.exit(1);
                }

            }

            threadMessage("Starting MessageLoop thread");
            long startTime = System.currentTimeMillis();
            Thread t = new Thread(new MessageLoop());
            t.start();

            threadMessage("Waiting for MessageLoop thread to finish");
            //loop until MessageLoop thread exits
            while (t.isAlive()) {
                threadMessage("Still waiting...");
                //Wait maximum of 1 second for MessageLoop thread to
                //finish.
                t.join(1000);
                if (((System.currentTimeMillis() - startTime) > patience) &&
                        t.isAlive()) {
                    threadMessage("Tired of waiting!");
                    t.interrupt();
                    //Shouldn't be long now -- wait indefinitely
                    t.join();
                }
            }
            threadMessage("Finally!");
        }
    }

同步
線程通信主要是通過訪問共享的字段以及這些字段所涉及的對象引用。這種通信的形式十分的高效,但它可能造成兩種錯誤:線程干涉和內存一致性錯誤。用於阻止這些錯誤的工具就是同步。
* 線程干預介紹了當多個線程訪問共享數據時產生的錯誤。
* 內存一致性錯誤介紹了指由對共享內存不一致的查看而導致的錯誤。
* 同步方法介紹了一種能夠有效地防止線程干預和內存一致性錯誤的常用方法。
* 隱含鎖和同步介紹了一種更通用的同步方法,並介紹了同步是如何基於隱含鎖的。
* 原子訪問介紹這種通用的不會受其它線程干預的操作概念。
線程干預
考慮這個叫Counter的簡單類
    class Counter {
        private int c = 0;

        public void increment() {
            c++;
        }

        public void decrement() {
            c--;
        }

        public int value() {
            return c;
        }
    }

Counter被設計成讓每次調用increment方法後c就加1,而每次調用decrement方法後c就減1。然而,如果一個Counter對象被多個線程所引用,那麼線程之前的干預可能不會使所期望的事情發生。
當在不同線程中的兩個操作交叉地作用於同一數據時,干預就發生了。這就是說兩個操作由多步組成,並且步調之間相互重疊。
看起來作用於Counter實例的操作不可能是交叉的,因爲這兩個關於c變量的操作都是一元的簡單語句。可是,如此簡單的語句也能夠被虛擬機解釋成多個步驟。我們不用檢查虛擬機所做的特定步驟--我們足以知道一元表達式c++可被分解成如下三步:
1. 獲取c的當前值。
2. 將這個被取出的值加1。
3. 將被加的值再放回c變量中。
表達式c--也能被進行相同地分解,除了將第二步的加替換爲減。
猜想在線程B調用decrement方法時,線程A調用了increment方法。如果c的初始值爲0,它們交叉的動作可能是如下順序:
1. 線程A:取出c。
2. 線程B:取出c。
3. 線程A:將取出的值加1,結果爲1。
4. 線程B:將取出的值減一,結果爲-1。
5. 線程A:將結果存於c中,c現在爲1。
6. 線程B:將結果存於c中,c現在爲-1。
線程A的結果丟失了,被線程B覆蓋了。這個特殊的交叉只是一種可能性。在不同的環境下,可能是線程B的結果丟失,也可能根本就沒有發生任何錯誤。因爲它們是不可能預知的,所以很難發現並修正線程干預缺陷。

內存一致性錯誤
當不同的線程觀察到本應相同但實際上不同的數據時,內存一致性錯誤就發生了。導致內存一致性錯誤的原因十分複雜並且超出了本教程的範圍。幸運地是,應用程序員並不需要了解這些原因的細節。所需要的就是一個避免它們的策略。
避免內存一致性錯誤的關鍵就是要理解"happens-before"的關係。這個關係保證了由一個特定的語句所寫的內存對其它特定的語句都是可見。爲了解它,可以考慮下面的例子。假設一個簡單的int型字段的定義與初始化:
int counter = 0;
counter字段被兩個線程,A和B,共享。假設線程A增加counter的值:
counter++;
很短的時間之後,線程B打印出counter的值:
System.out.println(counter);
如果這兩條語句是在同一個線程中執行,那就是可以很肯定地猜測被打印出的會是"1"。但如果在不同的線程中執行這兩條語句,被打印出的值可能正好是"0",因爲沒有什麼能保證線程A對counter的改變能被線程B看到--除非應用程序員在這兩條語句之間建立了"happens-before"關係。
有多種方式能夠創建"happens-before"關係。其中之一就是同步,我們將在接下來的一節中看到它。
我們已經看到了兩種建立"happens-before"關係的方法。
* 當一條語句調用Thread.start方法,一個新的線程執行的每條語句都有"happens-before"關係的語句與那些也有着"happens-before"關係。這些代碼的作用就是使新線程的創建對於其它的新線程是可見的。
* 當一個線程終止並在另一個線程中調用Thread.join導致返回,然後所有的由已終止的線程執行的語句伴着隨後成功join的所有語句都有"happens-before"關係。那麼在該線程中的代碼所產生的影響對於join進來的線程就是可見的。
要看創建"happens-before"關係的一列方法,可以參考java.util.concurrent包的摘要頁面。

同步方法
Java設計程序需要提供兩種基本的同步常用法:同步方法和同步語句。其中更爲複雜的一種,同步語句,將在下一節講述。本節是關於同步方法的。
使一個方法是可同步的,只要簡單地將關鍵字synchronized加到它的聲明中:
    public class SynchronizedCounter {
        private int c = 0;

        public synchronized void increment() {
            c++;
        }

        public synchronized void decrement() {
            c--;
        }

        public synchronized int value() {
            return c;
        }
    }

如果count是SynchronizedCounter的一個實例,那麼使這些方法同步將有兩個作用:
* 第一,對同一個對象中的同步方法進行交叉的調用就不可能了。當一個線程正在調用一個對象中的一個同步方法時,所有其它的調用該對象的同步方法的線程將被阻塞,直到第一個線程結束對該對象的工作。
* 第二,當同步方法存在,它就會與在同一對象中後序調用的方法自動地建立"happens-before"關係。
注意,構造器不能是可同步--對一個構造器使用關鍵字synchronized是一個語法錯誤。同步構造器沒有意義,因爲只有一個線程要創建對象,當它正在被構造時纔會訪問構造器。
警告:當構造一個將會在線程之間共享的對象時,要非常小心對象的引用過早地"溢出"。例如,假設你要維護一個叫instances的List去包含class的每個實例。你可能會嘗試着加入下面一行
instances.add(this);
到你的構造囂。但之後其它的線程可以在這個對象構造完成之前就可以instances去訪問該對象。
同步方法使一個簡單的防止線程干預和內存一致錯誤的策略成爲可能:如果一個對象對於一個以上的線程是可見的,所有針對該對象的變量的讀與寫都要通過同步方法。(有一個很重要的例外:在被構造之後就不能被修改的final字段,一旦它被創建,就能夠被非同步方法安全地讀取)。這種策略十分高效,但會出現活躍度問題,我們將在後面的教程中見到。
內部鎖與同步
同步是圍繞着一個被認爲是內部鎖或監視鎖的內部實體而建立(API規範經常就稱這個實體爲"監視器")。內部鎖在同步的兩個方面扮演着角色:強制排他地訪問一個對象的狀態,爲那些必須是可見的狀態建立"happen-before"關係。
每個對象都有一個與之關聯的內部鎖。一般地,一個線程要排他並一致地訪問一個對象的字段就必須在訪問它之前就獲得這個對象的內部鎖,在這個線程使用完之後就釋放這個內部鎖。在獲得鎖與釋放鎖之間的這段時間內,這個線程被認爲擁有這個內部鎖。一但線程擁有了內部鎖,其它的線程就不能再獲得相同的鎖了。當另一個線程試圖獲得這個鎖時,它將會被阻塞。
當線程釋放了一個內部鎖,在這個動作與後續想獲得同一個鎖的動作之間的"happens-before"關係就建立起來了。
在同步方法中的鎖
當線程調用了同步方法,它就自動地獲得這個方法所在對象的內部鎖,當這個方法返回時它就會釋放這個鎖。即使這個返回是由一個未捕獲的異常造成的,鎖也會被釋放。
你可能會對調用一個靜態的同步方法時所發生的事情感到驚訝,因爲靜態方法是與一個類,而不是一個對象,相關聯的。在這種情況下,線程要求獲得與這個類相關的Class對象的內部鎖。因此訪問類的靜態字段是被一個與作用於類的實例的鎖不同的鎖控制的。
同步語句
創建同步代碼的另一種方式是使用同步語句。與同步方法不同,同步語句必須要指定提供內部鎖的對象:
    public void addName(String name) {
        synchronized(this) {
            lastName = name;
            nameCount++;
        }
        nameList.add(name);
    }

在這個例子中,addName方法要對lastName和nameCount的修改進行同步,但也要避免同步地調用另一個對象中的方法(從同步代碼中調用另一個對象的方法會產生的問題將在Liveness章節中講述)。不用同步語句,就只能是一個隔離的非同步方法,其目的只是爲了調用nameList.add方法。
同步語句對使用細緻的同步去提高併發應用也是有用的。例如,假設類MsLunch有兩個實例字段,c1和c2,從來都沒有一起被使用過。這些字段的更新都必須是同步的,但沒有道理在交叉地對c2進行更新時防止對c1的更新--這樣做會創建不必要的阻塞而減少併發。我們創建兩個對象單獨地提供鎖,而不是使用同步方法或反而使用與this關聯的鎖。
    public class MsLunch {
        private long c1 = 0;
        private long c2 = 0;
        private Object lock1 = new Object();
        private Object lock2 = new Object();

        public void inc1() {
            synchronized(lock1) {
                c1++;
            }
        }

        public void inc2() {
            synchronized(lock2) {
                c2++;
            }
        }
    }

使用這種方法必須極其的小心。你必須非常地肯定交叉地訪問這些受影響的字段是安全的。
可重進入的同步
回憶一下,線程不能獲得被其它線程佔有的鎖。但線程可以獲得被它自己佔有的鎖。允許線程多次獲得相同的鎖就能夠形成可重進入的同步。這就能解釋這樣一種情況,當同步代碼直接或間接地調用了一個已經包含同步代碼的方法,但兩組代碼都使用相同的鎖。沒有可重進入的同步,同步代碼將不得不採取更多額外的預防措施去避免線程被自己阻塞。
原子訪問
在編程中,一個原子操作就是所有有效的動作一次性發生。原子操作不能在中間停止:它要麼完全發生,要麼完全不發生。原子操作不會有任何可見的副作用,直到該行爲完成。
我們已經看到了像c++這樣的加法表達式不是一個原子操作。非常簡單的表達甚至都可以被定義成能被分解爲其它操作的複雜操作。但是,有些操作你可以認爲它們是原子的:
* 讀和寫引用變量和大多數基本數據類型變量(除long和double之外的其它基本數據類型)
* 讀和寫被聲明爲volatile的變量(包括long和double型的變量)都是原子的。
原子操作不能被交叉地執行,因此使用它們可以不必擔心線程干預。然而,這並不能完全清除對原子操作進行同步的需要,因爲內存一致性錯誤的可能性仍然存在。使用volatile變量可以降低內存一致性錯誤的風險,因爲任何針對volatile變量的寫操作都與後續的針對該變量的讀操作之間建立了"happen-before"關係。這就意味着對一個voloatile變量的改變對於其它線程都是可見的。進一步說,這也意味着當一個線程讀一個volatile變量時,它不僅能看到該volatile變量的最新變化,也能看到導致該變化的代碼的副作用。
使簡潔的原子變量訪問比通過同步代碼訪問這些變量更加高效,但也要求應用程序員更加小心以避免內存一致性錯誤。額外的努力是否值得,取決於應用的規模與複雜度。
java.util.concurrent包中的一些類提供了一些不依賴於同步的原子方法。我們將在High Level Concurrency Objects一節中討論它們。
死鎖
    死鎖描述了一種兩個或以上的線程永久地相互等待而被阻塞的情形。這兒就有一個例子。
    Alphonse和Gaston是朋友,並且都很崇尚禮節。禮節的一條嚴格規則就是,當你向朋友鞠躬時,你必須保持鞠躬的姿勢直到你的朋友能有機會向你還以鞠躬。不幸地是,這條規則沒有說明這樣一種可能性,即兩個朋友可能在同時間相互鞠躬。
    public class Deadlock {
        static class Friend {
            private final String name;
                public Friend(String name) {
                    this.name = name;
                }
                public String getName() {
                    return this.name;
                }
                public synchronized void bow(Friend bower) {
                    System.out.format("%s: %s has bowed to me!%n",
                            this.name, bower.getName());
                    bower.bowBack(this);
                }
                public synchronized void bowBack(Friend bower) {
                    System.out.format("%s: %s has bowed back to me!%n",
                            this.name, bower.getName());
                }
            }

        public static void main(String[] args) {
            final Friend alphonse = new Friend("Alphonse");
            final Friend gaston = new Friend("Gaston");
            new Thread(new Runnable() {
                public void run() { alphonse.bow(gaston); }
            }).start();
            new Thread(new Runnable() {
                public void run() { gaston.bow(alphonse); }
            }).start();
        }
    }
    當Deadlock運行後,極有可能當兩個線程試圖調用bowBack時它們被阻塞了。沒有一種阻塞會結束,因爲每個線程都在另一方退出bow方法。
飢餓與活性鎖
    飢餓與活性鎖是沒有死鎖那麼普遍的問題,也仍然是每個併發軟件的設計者都可能遇到的問題。
    飢餓
    飢餓所描述的情形是指當一個線程不能正常地訪問到共享資源,也就不能得到進步。當共享資源被"貪婪"的線程長時間佔有時,這種情況就會發生。例如,假設一個對象提供了一個經常會耗費很長時間纔會返回的同步方法。如果一個線程頻繁地調用這個方法,其它也需要頻繁地同步訪問相同對象的線程就會經常被阻塞。
    活性鎖
    一個線程的行爲經常是對另一個線程的行爲的響應。如果另一個線程的行爲也是對另一個線程的行爲的響應,這時活性鎖可能就產生了。與死鎖比較,活性鎖線程是不能得到更進一步的進步。但是這些線程並沒有被阻塞--它們只是過分疲於應付彼此而不能恢復工作。就能比喻成在走廊中的兩個人都試圖通過對方:Alphonse向他的左邊移動以讓Gaston通過,此時Gaston則向他的右邊移動以讓Alphonse通過。看到他們仍然彼此阻塞着,Alphone就向他的右邊移動,此時Gaston向他的左邊移動。這樣,他們仍然各自阻塞着對方...
受保護的塊
    線程經常不得不調整它們的行爲。最常用的調整方式就是受保護的塊。在執行之前,這種塊開始時會輪詢查檢某個條件必須爲成立。爲了正確地做到這一點有許多步驟需要遵守。
    例如,假設guardedJoy方法將不會執行,直到共享變量joy被別的線程設置過。理論上,這樣的一個方法可以不停的循環直到條件滿足爲止。但這個循環不經濟,因爲在等待的時候它仍然在持續不停的運行。
    public void guardedJoy() {
        //Simple loop guard. Wastes processor time. Don't do this!
        while(!joy) {}
        System.out.println("Joy has been achieved!");
    }
    一種更高效的保護方式就是調用Object.wait方法去暫停當前的線程。調用wait方法不會返回直到另一個線程發出通知,說某個特定事件已經發生過了--儘管這個線程所等待的事件並不是必要的:
    public synchronized guardedJoy() {
        //This guard only loops once for each special event, which may not
        //be the event we're waiting for.
        while(!joy) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        System.out.println("Joy and efficiency have been achieved!");
    }
    注意:總是在循環內部調用wait方法去測試所等待的條件是否成立。不要猜想你所等待着的特殊條件中斷了,或者這個條件仍然成立。
    就像許多延緩執行的方法一樣,wait也會拋出InterruptedException異常。在這個例子中,我們可以忽略這個異常--我們僅關注joy的值。
    爲什麼guardedJoy的這個版本是可同步的?假設我們用調用wait方法的對象是d,當一個線程調用了wait方法,它必須擁有對象d的內部鎖--否則一個錯誤就會發生。在一個同步方法內部調用wait方法是一種獲得內部鎖的簡便途徑。
    當wait方法被調用了,該線程就釋放鎖並掛起執行。在以後的某個時間,另一個線程將會獲得相同的鎖並調用Object.notifyALL方法,通知所有正在等待這個鎖的線程某個重要的事情已經發生過了:
    public synchronized notifyJoy() {
        joy = true;
        notifyAll();
    }
    在第二個線程已經釋放鎖之後的某個時間,第一個線程重新獲得鎖並從wait方法的調用中返回以恢復執行。
    注意:還有另一個通知方法,notify,該方法只喚醒一個線程。因爲notify方法不允許你指定被喚醒的線程,所以它只用於大併發應用程序中--即,這個程序擁有大量的線程,這些線程又都做類似的事情。在這樣的應用中,你並不關心是哪個線程被喚醒了。
    讓我們使用受保護的塊去創建生產者-消費者應用。該種應用是在兩個線程中共享數據:生產者創建數據,而消費者使用數據。這兩個線程使用一個共享對象進行通信。協調是必須的:消費者線程在生產者線程交付這個數據之前不能試圖去獲取它,在消費者還沒有獲取老的數據之前生產者不能試圖交付新的數據。
    在這個例子中,數據是一系列的文本信息,它們通過類型爲Drop的對象進行共享:
    public class Drop {
        //Message sent from producer to consumer.
        private String message;
        //True if consumer should wait for producer to send message, false
        //if producer should wait for consumer to retrieve message.
        private boolean empty = true;

        public synchronized String take() {
            //Wait until message is available.
            while (empty) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            //Toggle status.
            empty = true;
            //Notify producer that status has changed.
            notifyAll();
            return message;
        }

        public synchronized void put(String message) {
            //Wait until message has been retrieved.
            while (!empty) {
                try {
                    wait();
                } catch (InterruptedException e) {}
            }
            //Toggle status.
            empty = false;
            //Store message.
            this.message = message;
            //Notify consumer that status has changed.
            notifyAll();
        }
    }

    生產者進程,由Producer類定義,傳遞一系列類似的信息。字符串"DONE"表示所有的信息都已經發出了。爲了模擬真實應用的不可能預知性,生產者線程在兩次發送信息之間會暫停一個隨機的時間間隔。

    import java.util.Random;

    public class Producer implements Runnable {
        private Drop drop;

        public Producer(Drop drop) {
            this.drop = drop;
        }

        public void run() {
            String importantInfo[] = {
                "Mares eat oats",
                "Does eat oats",
                "Little lambs eat ivy",
                "A kid will eat ivy too"
            };
            Random random = new Random();

            for (int i = 0; i < importantInfo.length; i++) {
                drop.put(importantInfo[i]);
                try {
                    Thread.sleep(random.nextInt(5000));
                } catch (InterruptedException e) {}
            }
            drop.put("DONE");
        }
    }
    消費者線程,由Consumer類定義,就獲得信息並把它們打印出來,直到獲得"DONE"對象爲止。該線程也會在隨機的時間間隔內暫停執行。

    import java.util.Random;
    
    public class Consumer implements Runnable {
        private Drop drop;

        public Consumer(Drop drop) {
            this.drop = drop;
        }

        public void run() {
            Random random = new Random();
            for (String message = drop.take(); ! message.equals("DONE");
                message = drop.take()) {
                System.out.format("MESSAGE RECEIVED: %s%n", message);
                try {
                    Thread.sleep(random.nextInt(5000));
                } catch (InterruptedException e) {}
            }
        }
    }

    最後就是main線程了,定義在了ProducerConsumerExample類中,該類將啓動生產者和消費者線程。

    public class ProducerConsumerExample {
        public static void main(String[] args) {
            Drop drop = new Drop();
            (new Thread(new Producer(drop))).start();
            (new Thread(new Consumer(drop))).start();
        }
    }

    注意:Drop類是爲了證明受保護的塊而寫的。爲了避免重新發明輪子,在嘗試測試你自己的數據共享對象之前可以先使用Java集合框架中的數據結構。
不可變對象
    如果一個對象的狀態在它被創建之後就不能修改了,這樣的對象就被認爲是不可變的。最大程度地依賴不可變對象是一個被廣泛接受的用來創建簡潔而可靠代碼的良好策略。
    不可變對象在併發應用中特別有用。由於它們的狀態不能改變,它們就不會有線程干預的困擾,也不會被觀察到不一致的狀態。
    應用程序員經常不使用不可變對象,因爲他們擔心創建一個新的對象而不是更新已有對象的狀態所付出的代價。創建對象的代價經常被高估了,而且與不可變對象相關的高效率也可抵消一些新建對象的代價。
    後面的子章節將使用一個使用可變實例的類,然後再從這個類派生出一個使用不可變實例的類。通過所做的這些,它們給出了一個進行這種轉變的通用規則,並證明了不可變對象的一些好處。
一個同步類的例子
    類SynchronizedRGB定義的對象用於表示色彩。每個對象用代表主色值的三個整數去表示色彩,並用一個字符串表示這種色彩的名稱。
    public class SynchronizedRGB {
        //Values must be between 0 and 255.
        private int red;
        private int green;
        private int blue;
        private String name;

    private void check(int red, int green, int blue) {
            if (red < 0 || red > 255
                    || green < 0 || green > 255
                    || blue < 0 || blue > 255) {
                throw new IllegalArgumentException();
            }
        }

        public SynchronizedRGB(int red, int green, int blue, String name) {
            check(red, green, blue);
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }

        public void set(int red, int green, int blue, String name) {
            check(red, green, blue);
            synchronized (this) {
                this.red = red;
                this.green = green;
                this.blue = blue;
                this.name = name;
            }
        }

        public synchronized int getRGB() {
            return ((red << 16) | (green << 8) | blue);
        }

        public synchronized String getName() {
            return name;
        }

        public synchronized void invert() {
            red = 255 - red;
            green = 255 - green;
            blue = 255 - blue;
            name = "Inverse of " + name;
        }
    }
    SynchronizedRGB必須小心地避免被觀察到不一致的狀態。例如,假設一個線程執行下面的代碼:
    SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black");
        ...
        int myColorInt = color.getRGB();      //Statement 1
        String myColorName = color.getName(); //Statement 2
    如果另一個線程在Statement 1之後而在Statement 2之前調用了color.set方法,那麼myColorInt的值就不匹配myColorName表示的色彩。爲了避免這種結果,這兩條語句必須綁在一起:
    synchronized (color) {
        int myColorInt = color.getRGB();
        String myColorName = color.getName();
    }
    這種不一致性只可能發生在不可變對象上--對於不可變版本的SynchronizedRGB就不會有這種問題。
定義不可變對象的策略
    下面的規則定義了一種簡單的創建不可變對象的策略。不是所有被標爲"immutable"的類都符合下面的這些規則。這也不是說這些類的創建者缺乏考慮--他們可能有好的理由去相信他們的類的實例在構造完畢之後就不會再改變了。
    1. 不要提供"setter"方法-修改字段的方法或由這些字段引用的對象。
    2. 將所有的字段設置爲final和private。
    3. 不允許子類去重載方法。實現這一點的最簡單的路徑就是將該類聲明爲final。更復雜一點兒的方法是聲明該類的構造器爲private,並通過工廠方法創建實例。
    4. 如果實例字段包含對可變對象的引用,就不允許這些對象被改變:
        * Don't provide methods that modify the mutable objects.
        * 不要提供修改這些對象的方法。
        * Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.
        * 不要共享對可變對象的引用。不要通過構造器存儲對外部可變對象的引用;如果必須那麼做,就創建一個拷貝,將引用存放到拷貝中。類似的,如果有必要避免在你的方法內部返回原始對象,可以爲你的內部可變對象創建拷貝。
    將該策略應用到SynchronizedRGB中就會產生如下步驟:
    1. 該類中有兩個setter方法。首先是set方法,無論使用何種方式改變該對象,在該類的不可變版本中都不可能再有它的位置了。其次就是invert方法,可以使用它創建一個新的對象而不是修改已有的對象。
    2. 所有的字段都已經是私有的了;再進一步使它們是final的。
    3. 將該類本身聲明爲final。
    4. 僅有一個引用其它對象的字段,而那個對象本身也是不可變的。因此,針對包含可變對象的狀態的防護手段都是不必要的了。
    做完這些之後,我們就有了ImmutableRGB:
    final public class ImmutableRGB {
        //Values must be between 0 and 255.
        final private int red;
        final private int green;
        final private int blue;
        final private String name;

        private void check(int red, int green, int blue) {
            if (red < 0 || red > 255
                    || green < 0 || green > 255
                    || blue < 0 || blue > 255) {
                throw new IllegalArgumentException();
            }
        }

        public ImmutableRGB(int red, int green, int blue, String name) {
            check(red, green, blue);
            this.red = red;
            this.green = green;
            this.blue = blue;
            this.name = name;
        }

        public int getRGB() {
            return ((red << 16) | (green << 8) | blue);
        }

        public String getName() {
            return name;
        }

        public ImmutableRGB invert() {
            return new ImmutableRGB(255 - red, 255 - green, 255 - blue,
                "Inverse of " + name);
        }
    }
高層次併發對象
    到目前爲止,本教程已經關注了在一開始就是Java平臺一部分的低層次API。這些API足以應付非常基本的工作,但對於更高級的工作,高層次組件是必需的。對於需要充分發掘當今多多處理器和多核系統的大規模併發應用就更是如此。
    在本節,我們將看到一些在Java平臺5.0版中引入的高級併發特性。這些特性中的大部分是在java.util.concurrent包中實現的。在Java集合框架中也有一些新的併發數據結構。
        * 支持鎖機制的鎖對象簡化了很多併發應用。
        * 執行器爲啓動和管理線程定義了一個高級API。由java.util.concurrent包提供的執行器的實現提供了適應於大規模應用的線程池管理。
        * 併發集合使得管理大型數據集合更爲簡易,並能大幅減少去同步的需求。
        * 原子變量擁有最小化對同步的需求並幫助避免內存一致性錯誤的特性。
鎖對象
    同步代碼依賴於一種簡單的可重入鎖。這種鎖方便使用,但有很多限制。java.util.concurrent.locks包提供了更爲複雜的鎖機制。我們不會細緻地測試這個包,而是關注它最基本的接口,Lock。
    鎖對象工作起來非常像由同步代碼使用的隱含鎖。使用隱含鎖時,在一個時間點只有一條線程能夠擁有鎖對象。通過與之相關聯的Condition對象,鎖對象也支持等待/喚醒機制。
    相對於隱含鎖,鎖對象最大的優勢就是它們可以退回(backs out)試圖去獲得某個鎖。如果一個鎖不能立刻或在一個時間限制(如果指定了)之前獲得的話,tryLock方法就會退回這一企圖。如果在獲得鎖之前,另一個線程發出了中斷信號,lockInterruptibly方法也會退回這一請求。
    讓我們使用鎖對象去解決在Liveness這一節中看到的死鎖問題。Alphonse和Gaston已經訓練了他們自己,能夠注意到朋友對鞠躬的反應。我們使用這樣一種方法去做改進,即要求Friend對象在應對鞠躬行爲之前,必須獲得兩個參與者的鎖。下面的源代碼就是改進後的模型,Safelock。爲了證明該方式的通用性,我們假設Alphonse和Gaston是如此地癡迷於他們新發現的安全地鞠躬的能力,以至於相互之間都不能停止向對方鞠躬。
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.Random;

    public class Safelock {
        static class Friend {
        private final String name;
        private final Lock lock = new ReentrantLock();

        public Friend(String name) {
            this.name = name;
        }

        public String getName() {
            return this.name;
        }

        public boolean impendingBow(Friend bower) {
            Boolean myLock = false;
            Boolean yourLock = false;
            try {
                myLock = lock.tryLock();
                yourLock = bower.lock.tryLock();
            } finally {
                if (! (myLock && yourLock)) {
                        if (myLock) {
                            lock.unlock();
                        }
                    if (yourLock) {
                        bower.lock.unlock();
                    }
                }
            }
            return myLock && yourLock;
        }
                    
        public void bow(Friend bower) {
            if (impendingBow(bower)) {
                try {
                    System.out.format("%s: %s has bowed to me!%n",
                        this.name, bower.getName());
                    bower.bowBack(this);
                } finally {
                    lock.unlock();
                    bower.lock.unlock();
                }
            } else {
                System.out.format("%s: %s started to bow to me, but" +
                    " saw that I was already bowing to him.%n",
                    this.name, bower.getName());
                }
            }

            public void bowBack(Friend bower) {
                System.out.format("%s: %s has bowed back to me!%n",
                    this.name, bower.getName());
            }
        }

        static class BowLoop implements Runnable {
            private Friend bower;
            private Friend bowee;

            public BowLoop(Friend bower, Friend bowee) {
                this.bower = bower;
                this.bowee = bowee;
            }

            public void run() {
                Random random = new Random();
                    for (;;) {
                try {
                    Thread.sleep(random.nextInt(10));
                } catch (InterruptedException e) {}
                    bowee.bow(bower);
                }
            }
        }

        public static void main(String[] args) {
            final Friend alphonse = new Friend("Alphonse");
            final Friend gaston = new Friend("Gaston");
            new Thread(new BowLoop(alphonse, gaston)).start();
            new Thread(new BowLoop(gaston, alphonse)).start();
        }
    }

執行器
    在所有之前的例子中,在由新線程執行的工作--由它的Runnable對象定義,和該線程本身--由Thread對象定義,之間有着緊密的聯繫。對於小規模應用,它能工作的很好,但在大規模應用中,就有必要將線程的管理與創建從應用的其它部分分離出來。封閉這部分功能的對象被稱作執行器。下面的子章節將細緻地描述執行器。
    * Executor接口定義了三種執行器類型。
    * Thread Pool是執行器最常用的實現。
Executor接口
    java.util.concurrent包定義了三種執行器接口:
    * Executor,是一種支持啓動新任務的簡單接口。
    * ExecutorService,是Executor接口的子接口,它加入了幫助管理包括單個任務和執行器本身的生命週期的功能。
    * ScheduledExecutorService,是ExecutorService的子接口,支持在未來時間和/或週期性執行任務。
    一般地,與執行器對象相關的變量都被聲明爲上述三個接口中的一個,而不是一個執行器類。
    Executor接口
    Executor接口提供了一個方法,execute,它被設計爲是通用的線程創建方式的替代品。如果r是一個Runnable對象,那麼e就是你用於替換r的Executor對象
    (new Thread(r)).start();
    和
    e.execute(r);
    然而,execute方法的定義缺乏規範。前面的低層次機制創建了一個新的線程並立即啓動它。但根據Executor的不同實現,execute可能也會做相同的事情,但更可能是使用一個已有的工人(worker)線程去執行r,或將r置於一個等待工人線程能被使用的隊列中。(我們將在Thread Pool一節中描述工人線程。)
    java.util.concurrent包中的執行器實現被設計爲使更高級的ExecutorService和ScheduledExecutorService能夠充分使用它,儘管它們也能夠與Executor接口一起工作。
    ExecutorService接口
    ExecutorService接口補充提供了一個與execute相似但功能更豐富的submit方法。與execute相同,submit方法也接受Runnable對象,但也接受Callable對象,該對象允許任務返回一個值。submit方法返回Future對象,該對象用於獲取Callable返回的值並管理Callable和Runnable任務的狀態。
    ExecutorService也提供了用於提交大量Callable對象集合的方法。最後,ExecutorService還提供了用於管理執行器關閉的一組方法。爲了支持立即關閉,任務應該要正確地處理中斷。
    ScheduledExecutorService接口
    ScheduledExecutorService接口爲它的父接口補充提供了與時間計劃有關的方法,使得能在指定延遲後執行Runnable或Callable任務。
線程池
    java.util.concurrent包中的大部分執行器實現都使用了由工人(worker)線程組成的線程池。這種線程獨立於Runnable和Callable任務而存在,常被用於執行多個任務。
    使用工人線程能夠最大限度的減小由於線程創建而產生的開銷。線程對象會佔用大量的內存,而在大規模應用中,分配和收回大量線程對象會造成大量的內存管理開銷。
    一種常用的線程池類型是固定數量線程池。這種池總是有特定數量的線程在執行;如果一個線程不知何故在它仍然被使用時終止了,它會立即被一個新的線程替代。任務通過內部隊列提交到線程池中。當活躍任務的數量超過線程的數量時,這種內部隊列會保存多餘的任務。
    固定數量線程池的一個重要的優點就是應用會慢慢退化地使用它。爲了理解這一點,考慮這樣的一個Web服務器的應用,每個HTTP請求被獨立的線程處理。如果應用爲每個新的HTTP請求創建一個新的線程,並且系統接到的請求數超出它能立即處理的數量,即當所有線程的開銷超過系統的承受能力時,該應用對此的反應就會是突然停止 。
    一種簡單地創建執行器的方法就是使用固定數量線程池,通過調用java.util.concurrent.Executors類的newFixedThreadPool工廠方法可以得到該線程池。Executors類也提供下面的工廠方法:
    * newCachedThreadPool方法創建一個有可擴展線程池的執行器。該執行器適用於會啓動許多短壽命任務的應用。
    * newSingleThreadExecutor方法創建在一個時間點只執行一個任務的執行器。
    * 有幾個工廠方法是上述執行器的ScheduledExecutorService版本。
    如果上述工廠方法提供的執行器沒有一個適合於你的需求,創建java.util.concurrent.ThreadPoolExecutor或java.util.concurrent.ScheduledThreadPoolExecutor的實例將給你另外的選擇。

併發集合
    java.util.concurrent包含一組Java集合框架額外的擴展。根據提供的集合接口十分容易把它們歸類爲:
    * BlockingQueue定義了一個先入先出的數據結構,當你試圖向一個已滿的隊列添加或向從一個已空的隊列中取出元素時,阻塞你或使你超時。
    * ConcurrentMap是java.util.Map的子接口,它定義了一些有用的原子操作。只有某個鍵存在時,這些操作才刪除或替換一個這個鍵-值對,或者只有當某個鍵不存在時,才能添加這個鍵-值對。使這些操作都是原子的,以幫助避免同步。標準而通用的ConcurrentMap實現是ConcurrentHashMap,它是HashMap的同步相似體。
    * ConcurrentNavigableMap is a subinterface of ConcurrentMap that supports approximate matches. The standard general-purpose implementation of ConcurrentNavigableMap is ConcurrentSkipListMap, which is a concurrent analog of TreeMap.
    * ConcurrentNavigableMap是ConcurrentMap的子接口,它支持近似符合。標準而通用的ConcurrentNavigableMap是ConcurrentSkipListMap,它是TreeMap的同步相似體。
    所有這些集合都爲了幫助避免內存一致性錯誤而在向集合中添加對象的操作與其後的訪問或刪除對象的操作之間定義了"Happens-Before"關係。

原子變量
    java.util.concurrent.atomic包定義了在單個變量中支持原子操作的類。所有的這些類都有get和set方法,這些方法就如同讀寫volatile變量那樣工作。即,一個set方法與任何隨其後的針對相同變量的get方法之間有"Happen-Before"對象。通過應用於整型原子變量的原子算術方法,原子的compareAndSet方法也戰士具有這樣的內存一致性特性。
    爲了看看如何使用這個包,讓我們回想之前爲了證明干預而使用過的類Counter:   
    class Counter {
        private int c = 0;

        public void increment() {
            c++;
        }

        public void decrement() {
            c--;
        }

        public int value() {
            return c;
        }
    }
    爲了防止線程干預的一種方法就是使它的方法可同步,如SynchronizedCounter裏的方法那樣:
    class SynchronizedCounter {
        private int c = 0;

        public synchronized void increment() {
            c++;
        }

        public synchronized void decrement() {
            c--;
        }

        public synchronized int value() {
            return c;
        }
    }
    對於這個簡單的類,同步是一個能夠被接受的解決方案。但對於更復雜的類,我們可能想避免不必要的同步的活躍度影響。使用AtomicInteger對象替代int字段允許我們在不求助同步的情況下就能防止線程干預。
    import java.util.concurrent.atomic.AtomicInteger;

    class AtomicCounter {
        private AtomicInteger c = new AtomicInteger(0);

        public void increment() {
            c.incrementAndGet();
        }

        public void decrement() {
            c.decrementAndGet();
        }

        public int value() {
            return c.get();
        }
    }
進一步地閱讀
    * Concurrent Programming in Java: Design Principles and Pattern (2nd Edition),Doug Lea著。這本綜合性著作的作者是一位卓越的專家,同時也是Java平臺併發框架的架構師。
    * Java Concurrency in Practice,Brian Goetz,Tim Peierls,Joshua Bloc

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