<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    John Jiang

    a cup of Java, cheers!
    https://github.com/johnshajiang/blog

       :: 首頁 ::  :: 聯系 :: 聚合  :: 管理 ::
      131 隨筆 :: 1 文章 :: 530 評論 :: 0 Trackbacks
    Java Tutorials -- Concurrency
        近一段時間在使用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 Bloch,Joseph Bowbeer,David Holmes和Doug Lea著。一本為貼近初學者而設計的實踐性指南。
        * Effective Java Programming Language Guide,Joshua Bloch著。雖然這是一本通用的程序設計指南,但其中關于線程的章節包含著并發編程必需的"最佳實踐"。
        * Concurrency: State Models & Java Programs (2nd Edition),Jeff Magee和eff Kramer著。通過模型化和實用的例子介紹了并發編程。

    posted on 2007-10-28 19:51 John Jiang 閱讀(2728) 評論(5)  編輯  收藏 所屬分類: JavaSEJavaConcurrency翻譯JavaTutorials

    評論

    # re: Java Tutorials -- Concurrency(譯) 2007-11-04 20:51 Sha Jiang
    最近忙于準備和參加Sun Tech Days,故未作更新。
    下周會盡力更新部分內容,但本章的內容較多,還是要慢慢來 ^_^  回復  更多評論
      

    # re: Java Tutorials -- Concurrency(譯) 2008-01-09 16:56 zhqworld
    看了同步方法和同步對象這一段,感覺翻譯的還可以,不過有的地方比較生硬,但像樓主這樣踏實求學的人現在的確不多了,期待更多好文,謝謝  回復  更多評論
      

    # re: Java Tutorials -- Concurrency(譯) 2008-01-09 20:13 sitinspring
    佩服。  回復  更多評論
      

    # re: Java Tutorials -- Concurrency(譯) 2008-01-10 09:17 Sha Jiang
    > 看了同步方法和同步對象這一段,感覺翻譯的還可以,不過有的地方比較生硬
    我有個"毛病",就是首選直譯。如果直譯能說通,就不用音譯了。
    這樣的話,就會有一些語句比較生硬了。
    不過,我是在學習翻譯的過程中,對于把握"直譯"或"意譯"的這個"度"也是正在提高中... *_*  回復  更多評論
      

    # re: Java Tutorials -- Concurrency(譯) 2008-04-14 13:59 Sha Jiang
    > 如果直譯能說通,就不用音譯了。
    有一處筆誤:"音譯"應為"意譯"。  回復  更多評論
      

    主站蜘蛛池模板: 亚洲国产成人乱码精品女人久久久不卡| 亚洲乱码日产一区三区| 国产激情久久久久影院老熟女免费| 亚洲精品色午夜无码专区日韩| 18以下岁毛片在免费播放| 亚洲综合av一区二区三区不卡| 中文字幕在亚洲第一在线| 97国产在线公开免费观看| 久久亚洲欧美国产精品| 亚洲日本在线看片| 国产伦精品一区二区三区免费下载 | 免费在线观看视频a| 57pao一国产成视频永久免费| 麻豆亚洲AV成人无码久久精品| 国产亚洲精品a在线观看app| 成人免费看吃奶视频网站| 暖暖在线视频免费视频| 亚洲av无码无线在线观看| 国产成人精品日本亚洲| 日韩免费视频在线观看| h视频在线观看免费完整版| 又粗又长又爽又长黄免费视频| 亚洲av极品无码专区在线观看| 国产亚洲人成网站在线观看| 毛片大全免费观看| 美女被cao网站免费看在线看| 激情婷婷成人亚洲综合| 国产.亚洲.欧洲在线| 亚洲AV人无码激艳猛片| 亚洲精品国自产拍在线观看| 野花高清在线观看免费3中文| 三年片在线观看免费| 污视频网站免费观看| 亚洲一区二区三区高清在线观看| 久久久亚洲欧洲日产国码农村| 亚洲精品综合久久| 国产黄色片在线免费观看| 美女视频黄免费亚洲| 香港a毛片免费观看| 毛片基地看看成人免费| 牛牛在线精品免费视频观看|