[Java][讀書筆記]多線程編程

前言:最近複習java,發現一本很好的資料,《J​a​v​a​2​參​考​大​全​ ​(​第​五​版​)​》 ​ ​H​e​r​b​e​r​t​.Schildt。書比較老了,06年的,一些內容有些舊了,翻譯也不是太好,但這本書的知識覆蓋面仍是很是全面,講述也是由淺入深,很值得參考的一本口碑很是好的書。我把一些內容加以整理,再配合着《think in Java》,把知識點梳理一遍。由於這本書的結構很好,因此目錄就以這本書爲主。後面看到更好的資料或有使用心得後會不斷更新此文章。
本文地址:http://www.cnblogs.com/rossoneri/p/4826777.htmlhtml

0. 線程的概念

和其餘多數計算機語言不一樣,Java內置支持多線程編程(multithreaded programming)java

多線程程序包含兩條或兩條以上併發運行的部分。程序中每一個這樣的部分都叫一個線程(thread),每一個線程都有獨立的執行路徑。所以,多線程是多任務處理的一種特殊形式。程序員

你必定知道多任務處理,由於它實際上被全部的現代操做系統所支持。然而,多任務處理有兩種大相徑庭的類型:基於進程的和基於線程的。認識二者的不一樣是十分重要的。編程

進程(process)本質上是一個執行的程序。所以,基於進程(process-based)的多任務處理的特色是容許你的計算機同時運行兩個或更多的程序。舉例來講,基於進程的多任務處理使你在運用文本編輯器的時候能夠同時運行Java編譯器。在基於進程的多任務處理中,程序是調度程序所分派的最小代碼單位。安全

基於線程(thread-based) 的多任務處理環境中,線程是最小的執行單位。這意味着一個程序能夠同時執行兩個或者多個任務的功能。例如,一個文本編輯器能夠在打印的同時格式化文本。markdown

因此,多進程程序處理「大圖片」,而多線程程序處理細節問題。網絡

多線程程序比多進程程序須要更少的管理費用。進程是重量級的任務,須要分配它們本身獨立的地址空間。進程間通訊是昂貴和受限的。進程間的轉換也是很須要花費的。另外一方面,線程是輕量級的選手。它們共享相同的地址空間而且共同分享同一個進程。線程間通訊是便宜的,線程間的轉換也是低成本的。當Java程序使用多進程任務處理環境時,多進程程序不受Java的控制,而多線程則受Java控制。數據結構

多線程幫助你寫出CPU最大利用率的高效程序,由於空閒時間保持最低。這對Java運行的交互式的網絡互連環境是相當重要的,由於空閒時間是公共的。舉個例子來講,網絡的數據傳輸速率遠低於計算機處理能力,本地文件系統資源的讀寫速度遠低於CPU的處理能力,固然,用戶輸入也比計算機慢不少。在傳統的單線程環境中,你的程序必須等待每個這樣的任務完成之後才能執行下一步——儘管CPU有不少空閒時間。多線程使你可以得到並充分利用這些空閒時間。多線程


1. 線程模型

Java運行系統在不少方面依賴於線程,全部的類庫設計都考慮到多線程。實際上,Java使用線程來使整個環境異步。這有利於經過防止CPU循環的浪費來減小無效部分。併發

爲更好的理解多線程環境的優點能夠將它與它的對照物相比較。單線程系統的處理途徑是使用一種叫做輪詢的事件循環方法。在該模型中,單線程控制在一無限循環中運行,輪詢一個事件序列來決定下一步作什麼。一旦輪詢裝置返回信號代表,已準備好讀取網絡文件,事件循環調度控制管理到適當的事件處理程序。直到事件處理程序返回,系統中沒有其餘事件發生。這就浪費了CPU時間。這致使了程序的一部分獨佔了系統,阻止了其餘事件的執行。總的來講,單線程環境,當一個線程由於等待資源時阻塞(block,掛起執行),整個程序中止運行。

Java多線程的優勢在於取消了主循環/輪詢機制。一個線程能夠暫停而不影響程序的其餘部分。例如,當一個線程從網絡讀取數據或等待用戶輸入時產生的空閒時間能夠被利用到其餘地方。多線程容許活的循環在每一幀間隙中沉睡一秒而不暫停整個系統。在Java程序中出現線程阻塞,僅有一個線程暫停,其餘線程繼續運行。

線程存在於好幾種狀態。線程能夠正在運行(running)。只要得到CPU時間它就能夠運行。運行的線程能夠被掛起(suspend),並臨時中斷它的執行。一個掛起的線程能夠被恢復(resume),容許它從中止的地方繼續運行。一個線程能夠在等待資源時被阻塞(block)。在任什麼時候候,線程能夠終止(terminate),這當即中斷了它的運行。一旦終止,線程不能被恢復。

線程優先級

Java給每一個線程安排優先級以決定與其餘線程比較時該如何對待該線程。線程優先級是詳細說明線程間優先關係的整數。做爲絕對值,優先級是毫無心義的;當只有一個線程時,優先級高的線程並不比優先權低的線程運行的快。相反,線程的優先級是用來決定什麼時候從一個運行的線程切換到另外一個。這叫「上下文轉換」(context switch)。決定上下文轉換髮生的規則很簡單:

  • 線程能夠自動放棄控制。在I/O未決定的狀況下,睡眠或阻塞由明確的讓步來完成。在這種假定下,全部其餘的線程被檢測,準備運行的最高優先級線程被授予CPU。
  • 線程能夠被高優先級的線程搶佔。在這種狀況下,低優先級線程不主動放棄,處理器只是被先佔——不管它正在幹什麼——處理器被高優先級的線程佔據。基本上,一旦高優先級線程要運行,它就執行。這叫作有優先權的多任務處理。

當兩個相同優先級的線程競爭CPU週期時,情形有一點複雜。具體與操做系統有關。

不一樣的操做系統下等優先級線程的上下文轉換可能會產生錯誤。

同步性

由於多線程在你的程序中引入了一個異步行爲,因此在你須要的時候必須有增強同步性的方法。舉例來講,若是你但願兩個線程相互通訊並共享一個複雜的數據結構,例如鏈表序列,你須要某些方法來確保它們沒有相互衝突。也就是說,你必須防止一個線程寫入數據而另外一個線程正在讀取鏈表中的數據。爲此目的,Java在進程間同步性的老模式基礎上實行了另外一種方法:管程(monitor)。管程是一種由C.A.R.Hoare首先定義的控制機制。

你能夠把管程想象成一個僅控制一個線程的小盒子。一旦線程進入管程,全部線程必須等待直到該線程退出了管程。用這種方法,管程能夠用來防止共享的資源被多個線程操縱。

不少多線程系統把管程做爲程序必須明確的引用和操做的對象。Java提供一個清晰的解決方案。沒有「Monitor」類;相反,每一個對象都擁有本身的隱式管程,當對象的同步方法被調用時管程自動載入。一旦一個線程包含在一個同步方法中,沒有其餘線程能夠調用相同對象的同步方法。這就使你能夠編寫很是清晰和簡潔的多線程代碼,由於同步支持是語言內置的。

消息傳遞

在你把程序分紅若干線程後,你就要定義各線程之間的聯繫。用大多數其餘語言規劃時,你必須依賴於操做系統來確立線程間通訊。這樣固然增長花費。然而,Java提供了多線程間談話清潔的、低成本的途徑——經過調用全部對象都有的預先肯定的方法。Java的消息傳遞系統容許一個線程進入一個對象的一個同步方法,而後在那裏等待,直到其餘線程明確通知它出來。

Thread 類和Runnable 接口

Java的多線程系統創建於Thread類,它的方法,它的共伴接口Runnable基礎上。Thread類封裝了線程的執行。既然你不能直接引用運行着的線程的狀態,你要經過它的代理處理它,因而Thread 實例產生了。爲建立一個新的線程,你的程序必須擴展Thread 或實現Runnable接口。

Thread類定義了好幾種方法來幫助管理線程。本章用到的方法以下表所示:

管理線程的方法


| 方法 |意義 |
| ------- | ------- |
| getName | 得到線程名稱 |
| getPriority | 得到線程優先級 |
| isAlive | 斷定線程是否仍在運行 |
| join | 等待一個線程終止 |
| run | 線程的入口點 |
| sleep | 在一段時間內掛起線程 |
| start | 經過調用運行方法來啓動線程 |
(博客園markdown不支持表格顯示?)


2. 主線程

當Java程序啓動時,一個線程馬上運行,該線程一般叫作程序的主線程(main thread),由於它是程序開始時就執行的。主線程的重要性體如今兩方面:

  • 它是產生其餘子線程的線程
  • 一般它必須最後完成執行,由於它執行各類關閉動做

儘管主線程在程序啓動時自動建立,但它能夠由一個Thread對象控制。爲此,你必須調用方法currentThread()得到它的一個引用,currentThread()是Thread類的公有的靜態方法。它的一般形式以下:

static Thread currentThread( )

該方法返回一個調用它的線程的引用。一旦你得到主線程的引用,你就能夠像控制其餘線程那樣控制主線程。

讓咱們從複習下面例題開始:

public class CurrentThreadDemo {
    public static void main(String args[]) {
        Thread t = Thread.currentThread();
        System.out.println("Current thread: " + t);
        // change the name of the thread
        t.setName("My Thread");
        System.out.println("After name change: " + t);
        try {
            for (int n = 5; n > 0; n--) {
                System.out.println(n);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }
    }
}

在本程序中,當前線程(天然是主線程)的引用經過調用currentThread()得到,該引用保存在局部變量t中。而後,程序顯示了線程的信息。接着程序調用setName()改變線程的內部名稱。線程信息又被顯示。而後,一個循環數從5開始遞減,每數一次暫停一秒。暫停是由sleep()方法來完成的。Sleep()語句明確規定延遲時間是1毫秒。注意循環外的try/catch塊。

Thread類的sleep()方法可能引起一個InterruptedException異常。這種情形會在其餘線程想要打攪沉睡線程時發生。本例只是打印了它是否被打斷的消息。在實際的程序中,你必須靈活處理此類問題。下面是本程序的輸出:

Current thread: Thread[main,5,main]
After name change: Thread[My Thread,5,main]
5
4
3
2
1

注意t做爲語句println()中參數運用時輸出的產生。該顯示順序:線程名稱,優先級以及組的名稱。默認狀況下,主線程的名稱是main。它的優先級是5,這也是默認值,main也是所屬線程組的名稱。一個線程組(thread group)是一種將線程做爲一個總體集合的狀態控制的數據結構。這個過程由專有的運行時環境來處理,在此就不贅述了。線程名改變後,t又被輸出。此次,顯示了新的線程名。

讓咱們更仔細的研究程序中Thread類定義的方法。sleep()方法按照毫秒級的時間指示使線程從被調用到掛起。它的一般形式以下:

static void sleep(long milliseconds) throws InterruptedException

掛起的時間被明肯定義爲毫秒。該方法可能引起InterruptedException異常。

sleep()方法還有第二種形式,顯示以下,該方法容許你指定時間是以毫秒仍是以納秒爲週期。

static void sleep(long milliseconds, int nanoseconds) throws InterruptedException

第二種形式僅當容許以納秒爲時間週期時可用。如上述程序所示,你能夠用setName()設置線程名稱,用getName()來得到線程名稱(該過程在程序中沒有體現)。這些方法都是Thread 類的成員,聲明以下:

final void setName(String threadName)
final String getName( )

這裏,threadName 特指線程名稱。


3. 建立線程

大多數狀況,經過實例化一個Thread對象來建立一個線程。Java定義了兩種方式:

  • 實現Runnable 接口
  • 能夠繼承Thread類

實現Runnable接口

建立線程的最簡單的方法就是建立一個實現Runnable 接口的類。Runnable抽象了一個執行代碼單元。你能夠經過實現Runnable接口的方法建立每個對象的線程。爲實現Runnable 接口,一個類僅需實現一個run()的簡單方法,該方法聲明以下:

public void run( )

在run()中能夠定義代碼來構建新的線程。理解下面內容是相當重要的:run()方法可以像主線程那樣調用其餘方法,引用其餘類,聲明變量。僅有的不一樣是run()在程序中確立另外一個併發的線程執行入口。當run()返回時,該線程結束。

在你已經建立了實現Runnable接口的類之後,你要在類內部實例化一個Thread類的對象。Thread 類定義了好幾種構造函數。咱們會用到的以下:

Thread(Runnable threadOb, String threadName)

該構造函數中,threadOb是一個實現Runnable接口類的實例。這定義了線程執行的起點。新線程的名稱由threadName定義。

創建新的線程後,它並不運行直到調用了它的start()方法,該方法在Thread 類中定義。本質上,start() 執行的是一個對run()的調用。Start()方法聲明以下:

void start( )

下面的例子是建立一個新的線程並啓動它運行:

// Create a second thread.
class NewThread implements Runnable {
    Thread t;

    NewThread() {
        // Create a new, second thread
        t = new Thread(this, "Demo Thread");
        System.out.println("Child thread: " + t);
        t.start(); // Start the thread
    }

    // This is the entry point for the second thread.
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Child Thread: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Child interrupted.");
        }
        System.out.println("Exiting child thread.");
    }
}

public class Demo {
    public static void main(String args[]) {
        new NewThread(); // create a new thread
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Main Thread: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

在NewThread 構造函數中,新的Thread對象由下面的語句建立::

t = new Thread(this, "Demo Thread");

經過前面的語句this 代表在this對象中你想要新的線程調用run()方法。而後,start() 被調用,以run()方法爲開始啓動了線程的執行。這使子線程for 循環開始執行。調用start()以後,NewThread 的構造函數返回到main()。當主線程被恢復,它到達for 循環。兩個線程繼續運行,共享CPU,直到它們的循環結束。該程序的輸出以下:

Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting.

如前面提到的,在多線程程序中,一般主線程必須是結束運行的最後一個線程。實際上,一些老的JVM,若是主線程先於子線程結束,Java的運行時間系統就可能「掛起」。前述程序保證了主線程最後結束,由於主線程沉睡週期1000毫秒,而子線程僅爲500毫秒。這就使子線程在主線程結束以前先結束。簡而言之,你將看到等待線程結束的更好途徑。

擴展Thread

建立線程的另外一個途徑是建立一個新類來擴展Thread類,而後建立該類的實例。當一個類繼承Thread時,它必須重載run()方法,這個run()方法是新線程的入口。它也必須調用start()方法去啓動新線程執行。下面用擴展thread類重寫前面的程序:

// Create a second thread by extending Thread
class NewThread extends Thread {
    NewThread() {
        // Create a new, second thread
        super("Demo Thread");
        System.out.println("Child thread: " + this);
        start(); // Start the thread
    }

    // This is the entry point for the second thread.
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Child Thread: " + i);
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Child interrupted.");
        }
        System.out.println("Exiting child thread.");
    }
}

public class Demo {
    public static void main(String args[]) {
        new NewThread(); // create a new thread
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println("Main Thread: " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        System.out.println("Main thread exiting.");
    }
}

該程序生成和前述版本相同的輸出。子線程是由實例化NewThread對象生成的,該對象從Thread類派生。注意NewThread 中super()的調用。該方法調用了下列形式的Thread構造函數:

public Thread(String threadName)

這裏,threadName指定線程名稱。

Thread類定義了多種方法能夠被派生類複寫。對於全部的方法,唯一的必須被複寫的是run()方法。這固然是實現Runnable接口所需的一樣的方法。不少Java程序員認爲類僅在它們被增強或修改時應該被擴展。所以,若是你不復寫Thread的其餘方法時,最好只實現Runnable 接口。這固然由你決定。然而,在本章的其餘部分,咱們應用實現runnable接口的類來建立線程。


4. 建立多線程

到目前爲止,咱們僅用到兩個線程:主線程和一個子線程。然而,你的程序能夠建立所需的更多線程。例如,下面的程序建立了三個子線程:
到目前爲止,咱們僅用到兩個線程:主線程和一個子線程。然而,你的程序能夠建立所需的更多線程。例如,下面的程序建立了三個子線程:

// Create multiple threads.
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;

    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start(); // Start the thread
    }

    // This is the entry point for thread.
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(name + "Interrupted");
        }
        System.out.println(name + " exiting.");
    }
}

public class Demo {
    public static void main(String args[]) {
        new NewThread("One"); // start threads
        new NewThread("Two");
        new NewThread("Three");
        try {
            // wait for other threads to end
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

程序輸出:

New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
One: 5
New thread: Thread[Three,5,main]
Two: 5
Three: 5
One: 4
Three: 4
Two: 4
Two: 3
Three: 3
One: 3
Two: 2
One: 2
Three: 2
Two: 1
Three: 1
One: 1
One exiting.
Two exiting.
Three exiting.
Main thread exiting.

如你所見,一旦啓動,全部三個子線程共享CPU。注意main()中對sleep(10000)的調用。這使主線程沉睡十秒確保它最後結束。


5. 使用 isAlive() 和 join()

如前所述,一般你但願主線程最後結束。在前面的例子中,這點是經過在main()中調用sleep()來實現的,通過足夠長時間的延遲以確保全部子線程都先於主線程結束。然而,這不是一個使人滿意的解決方法,它也帶來一個大問題:一個線程如何知道另外一線程已經結束?幸運的是,Thread類提供了回答此問題的方法。

有兩種方法能夠斷定一個線程是否結束。第一,能夠在線程中調用isAlive()。這種方法由Thread定義,它的一般形式以下:

final boolean isAlive( )

若是所調用線程仍在運行,isAlive()方法返回true,若是不是則返回false。但isAlive()不多用到,等待線程結束的更經常使用的方法是調用join(),描述以下:

final void join( ) throws InterruptedException

該方法等待所調用線程結束。該名字來自於要求線程等待直到指定線程參與的概念。join()的附加形式容許給等待指定線程結束定義一個最大時間。下面是前面例子的改進版本。運用join()以確保主線程最後結束。一樣,它也演示了isAlive()方法。

// Using join() to wait for threads to finish.
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;

    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start(); // Start the thread
    }

    // This is the entry point for thread.
    public void run() {
        try {
            for (int i = 5; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted.");
        }
        System.out.println(name + " exiting.");
    }
}

class DemoJoin {
    public static void main(String args[]) {
        NewThread ob1 = new NewThread("One");
        NewThread ob2 = new NewThread("Two");
        NewThread ob3 = new NewThread("Three");
        System.out.println("Thread One is alive: " + ob1.t.isAlive());
        System.out.println("Thread Two is alive: " + ob2.t.isAlive());
        System.out.println("Thread Three is alive: " + ob3.t.isAlive());
        // wait for threads to finish
        try {
            System.out.println("Waiting for threads to finish.");
            ob1.t.join();
            ob2.t.join();
            ob3.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Thread One is alive: " + ob1.t.isAlive());
        System.out.println("Thread Two is alive: " + ob2.t.isAlive());
        System.out.println("Thread Three is alive: " + ob3.t.isAlive());
        System.out.println("Main thread exiting.");
    }
}

運行結果:

New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
One: 5
New thread: Thread[Three,5,main]
Two: 5
Thread One is alive: true
Thread Two is alive: true
Three: 5
Thread Three is alive: true
Waiting for threads to finish.
Two: 4
Three: 4
One: 4
One: 3
Two: 3
Three: 3
Two: 2
Three: 2
One: 2
Two: 1
One: 1
Three: 1
Three exiting.
Two exiting.
One exiting.
Thread One is alive: false
Thread Two is alive: false
Thread Three is alive: false
Main thread exiting.

如你所見,調用join()後返回,線程終止執行。


6. 線程優先級

線程優先級被線程調度用來斷定什麼時候每一個線程容許運行。理論上,優先級高的線程比優先級低的線程得到更多的CPU時間。實際上,線程得到的CPU時間一般由包括優先級在內的多個因素決定(例如,一個實行多任務處理的操做系統如何更有效的利用CPU時間)。一個優先級高的線程天然比優先級低的線程優先。舉例來講,當低優先級線程正在運行,而一個高優先級的線程被恢復(例如從沉睡中或等待I/O中),它將搶佔低優先級線程所使用的CPU。

理論上,等優先級線程有同等的權利使用CPU。但你必須當心了。記住,Java是被設計成能在不少環境下工做的。一些環境下實現多任務處理從本質上與其餘環境不一樣。爲安全起見,等優先級線程偶爾也受控制。這保證了全部線程在無優先級的操做系統下都有機會運行。實際上,在無優先級的環境下,多數線程仍然有機會運行,由於不少線程不可避免的會遭遇阻塞,例如等待輸入輸出。遇到這種情形,阻塞的線程掛起,其餘線程運行。可是若是你但願多線程執行的順利的話,最好不要採用這種方法。一樣,有些類型的任務是佔CPU的。對於這些支配CPU類型的線程,有時你但願可以支配它們,以便使其餘線程能夠運行。

設置線程的優先級,用setPriority()方法,該方法也是Tread 的成員。它的一般形式爲:

final void setPriority(int level)

這 裏 , level 指 定了對所調用的線程的新的優先權的設置。Level的值必須在MIN_PRIORITY到MAX_PRIORITY範圍內。一般,它們的值分別是1和10。要返回一個線程爲默認的優先級,指定NORM_PRIORITY,一般值爲5。這些優先級在Thread中都被定義爲final型變量。

你能夠經過調用Thread的getPriority()方法來得到當前的優先級設置。該方法以下:

final int getPriority( )

當涉及調度時,Java的執行能夠有本質上不一樣的行爲。Windows 95/98/NT/2000 的工做或多或少如你所願。但其餘版本可能工做的徹底不一樣。大多數矛盾發生在你使用有優先級行爲的線程,而不是協同的騰出CPU時間。最安全的辦法是得到可預先性的優先權,Java得到跨平臺的線程行爲的方法是自動放棄對CPU的控制。

下面的例子闡述了兩個不一樣優先級的線程,運行於具備優先權的平臺,這與運行於無優先級的平臺不一樣。一個線程經過Thread.NORM_PRIORITY設置了高於普通優先級兩級的級數,另外一線程設置的優先級則低於普通級兩級。兩線程被啓動並容許運行10秒。每一個線程執行一個循環,記錄反覆的次數。10秒後,主線程終止了兩線程。每一個線程通過循環的次數被顯示。

// Demonstrate thread priorities.
class clicker implements Runnable {
     int click = 0;
    Thread t;
    private volatile boolean running = true;
    public clicker(int p) {
        t = new Thread(this);
        t.setPriority(p);
    }

    public void run() {
        while (running) {
            click++;
        }
    }

    public void stop() {
        running = false;
    }

    public void start() {
        t.start();
    }
}

class Demo {
    public static void main(String args[]) {
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        clicker hi = new clicker(Thread.NORM_PRIORITY + 2);
        clicker lo = new clicker(Thread.NORM_PRIORITY - 2);
        lo.start();
        hi.start();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted.");
        }
        lo.stop();
        hi.stop();
        // Wait for child threads to terminate.
        try {
            hi.t.join();
            lo.t.join();
        } catch (InterruptedException e) {
            System.out.println("InterruptedException caught");
        }

        System.out.println("Low-priority thread: " + lo.click);
        System.out.println("High-priority thread: " + hi.click);
    }
}

我在Mac上運行結果是:

Low-priority thread: 158021835
High-priority thread: 111098732

原文狀況比較早,以下:
該程序在Windows 98下運行的輸出,代表線程確實上下轉換,甚至既不屈從於CPU,也不被輸入輸出阻塞。優先級高的線程得到大約90%的CPU時間。

Low-priority thread: 4408112
High-priority thread: 589626904

固然,該程序的精確的輸出結果依賴於你的CPU的速度和運行的其餘任務的數量。當一樣的程序運行於無優先級的系統,將會有不一樣的結果。

上述程序還有個值得注意的地方。注意running前的關鍵字volatile。儘管volatile 在下章會被很仔細的討論,用在此處以確保running的值在下面的循環中每次都獲得驗證。

while (running) {
    click++;
}

若是不用volatile,Java能夠自由的優化循環:running的值被存在CPU的一個寄存器中,每次重複不必定須要複檢。volatile的運用阻止了該優化,告知Java running能夠改變,改變方式並不以直接代碼形式顯示。

7. 線程同步

當兩個或兩個以上的線程須要共享資源,它們須要某種方法來肯定資源在某一刻僅被一個線程佔用。達到此目的的過程叫作同步(synchronization)。像你所看到的,Java爲此提供了獨特的,語言水平上的支持。

同步的關鍵是管程(也叫信號量semaphore)的概念。管程是一個互斥獨佔鎖定的對象,或稱互斥體(mutex)。在給定的時間,僅有一個線程能夠得到管程。當一個線程須要鎖定,它必須進入管程。全部其餘的試圖進入已經鎖定的管程的線程必須掛起直到第一個線程退出管程。這些其餘的線程被稱爲等待管程。一個擁有管程的線程若是願意的話能夠再次進入相同的管程。

若是你用其餘語言例如C或C++時用到過同步,你會知道它用起來有一點詭異。這是由於不少語言它們本身不支持同步。相反,對同步線程,程序必須利用操做系統源語。幸運的是Java經過語言元素實現同步,大多數的與同步相關的複雜性都被消除。

你能夠用兩種方法同步化代碼。二者都包括synchronized關鍵字的運用,下面分別說明這兩種方法。

使用同步方法

Java中同步是簡單的,由於全部對象都有它們與之對應的隱式管程。進入某一對象的管程,就是調用被synchronized關鍵字修飾的方法。當一個線程在一個同步方法內部,全部試圖調用該方法(或其餘同步方法)的同實例的其餘線程必須等待。爲了退出管程,並放棄對對象的控制權給其餘等待的線程,擁有管程的線程僅需從同步方法中返回。

爲理解同步的必要性,讓咱們從一個應該使用同步卻沒有用的簡單例子開始。下面的程序有三個簡單類。首先是Callme,它有一個簡單的方法call( )。call( )方法有一個名爲msg的String參數。該方法試圖在方括號內打印msg 字符串。有趣的事是在調用call( ) 打印左括號和msg字符串後,調用Thread.sleep(1000),該方法使當前線程暫停1秒。

下一個類的構造函數Caller,引用了Callme的一個實例以及一個String,它們被分別存在target 和 msg 中。構造函數也建立了一個調用該對象的run( )方法的新線程。該線程當即啓動。Caller類的run( )方法經過參數msg字符串調用Callme實例target的call( ) 方法。最後,Synch類由建立Callme的一個簡單實例和Caller的三個具備不一樣消息字符串的實例開始。Callme的同一實例傳給每一個Caller實例。

// This program is not synchronized.
class Callme {
    void call(String msg) {
        System.out.print("[" + msg);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Interrupted");
        }
        System.out.println("]");
    }
}

class Caller implements Runnable {
    String msg;
    Callme target;
    Thread t;

    public Caller(Callme targ, String s) {
        target = targ;
        msg = s;
        t = new Thread(this);
        t.start();
    }

    public void run() {
        target.call(msg);
    }
}

class Synch {
    public static void main(String args[]) {
        Callme target = new Callme();
        Caller ob1 = new Caller(target, "Hello");
        Caller ob2 = new Caller(target, "Synchronized");
        Caller ob3 = new Caller(target, "World");
        // wait for threads to end
        try {
            ob1.t.join();
            ob2.t.join();
            ob3.t.join();
        } catch (InterruptedException e) {
            System.out.println("Interrupted");
        }
    }
}

該程序的輸出以下:

[Hello[World[Synchronized]]]

在本例中,經過調用sleep( ),call( )方法容許執行轉換到另外一個線程。該結果是三個消息字符串的混合輸出。該程序中,沒有阻止三個線程同時調用同一對象的同一方法的方法存在。這是一種競爭,由於三個線程爭着完成方法。例題用sleep( )使該影響重複和明顯。在大多數狀況,競爭是更爲複雜和不可預知的,由於你不能肯定什麼時候上下文轉換會發生。這使程序時而運行正常時而出錯。

爲達到上例所想達到的目的,必須有權連續的使用call( )。也就是說,在某一時刻,必須限制只有一個線程能夠支配它。爲此,你只需在call( ) 定義前加上關鍵字synchronized,以下:

class Callme {
    synchronized void call(String msg) {
        ...
    }
}

這防止了在一個線程使用call( )時其餘線程進入call( )。在synchronized加到call( )前面之後,程序輸出以下:

[Hello]
[World]
[Synchronized]

任什麼時候候在多線程狀況下,你有一個方法或多個方法操縱對象的內部狀態,都必須用synchronized 關鍵字來防止狀態出現競爭。記住,一旦線程進入實例的同步方法,沒有其餘線程能夠進入相同實例的同步方法。然而,該實例的其餘不一樣步方法卻仍然能夠被調用。

同步語句

儘管在建立的類的內部建立同步方法是得到同步的簡單和有效的方法,但它並不是在任什麼時候候都有效。這其中的緣由,請跟着思考。假設你想得到不爲多線程訪問設計的類對象的同步訪問,也就是,該類沒有用到synchronized方法。並且,該類不是你本身,而是第三方建立的,你不能得到它的源代碼。這樣,你不能在相關方法前加synchronized修飾符。怎樣才能使該類的一個對象同步化呢?很幸運,解決方法很簡單:你只需將對這個類定義的方法的調用放入一個synchronized塊內就能夠了。

下面是synchronized語句的普通形式:

synchronized(object) {
    // statements to be synchronized
}

其中,object是被同步對象的引用。若是你想要同步的只是一個語句,那麼不須要花括號。一個同步塊確保對object成員方法的調用僅在當前線程成功進入object管程後發生。
下面是前面程序的修改版本,在run( )方法內用了同步塊:

// synchronize calls to call()
    public void run() {
        synchronized (target) { // synchronized block
            target.call(msg);
        }
    }

這裏,call( )方法沒有被synchronized修飾。而synchronized是在Caller類的run( )方法中聲明的。這能夠獲得上例中一樣正確的結果,由於每一個線程運行前都等待先前的一個線程結束。


8. 線程間通訊

上述例題無條件的阻塞了其餘線程異步訪問某個方法。Java對象中隱式管程的應用是很強大的,可是你能夠經過線程間通訊達到更微妙的境界。這在Java中是尤其簡單的。

像前面所討論過的,多線程經過把任務分紅離散的和合乎邏輯的單元代替了事件循環程序。線程還有第二優勢:它遠離了輪詢。輪詢一般由重複監測條件的循環實現。一旦條件成立,就要採起適當的行動。這浪費了CPU時間。舉例來講,考慮經典的序列問題,當一個線程正在產生數據而另外一個程序正在消費它。爲使問題變得更有趣,假設數據產生器必須等待消費者完成工做才能產生新的數據。在輪詢系統,消費者在等待生產者產生數據時會浪費不少CPU週期。一旦生產者完成工做,它將啓動輪詢,浪費更多的CPU時間等待消費者的工做結束,如此下去。很明顯,這種情形不受歡迎。

爲避免輪詢,Java包含了經過wait( ),notify( )和notifyAll( )方法實現的一個進程間通訊機制。這些方法在對象中是用final方法實現的,因此全部的類都含有它們。這三個方法僅在synchronized方法中才能被調用。儘管這些方法從計算機科學遠景方向上來講具備概念的高度先進性,實際中用起來是很簡單的:

  • wait( ) 告知被調用的線程放棄管程進入睡眠直到其餘線程進入相同管程而且調用notify( )。
  • notify( ) 恢復相同對象中第一個調用 wait( ) 的線程。
  • notifyAll( ) 恢復相同對象中全部調用 wait( ) 的線程。具備最高優先級的線程最早運行。

這些方法在Object中被聲明,以下所示:

final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )

wait( )存在的另外的形式容許你定義等待時間。

下面的例子程序錯誤的實行了一個簡單生產者/消費者的問題。它由四個類組成:Q,設法得到同步的序列;Producer,產生排隊的線程對象;Consumer,消費序列的線程對象;以及PC,建立單個Q,Producer,和Consumer的小類。

// An incorrect implementation of a producer and consumer.
class Q {
    int n;

    synchronized int get() {
        System.out.println("Got: " + n);
        return n;
    }

    synchronized void put(int n) {
        this.n = n;
        System.out.println("Put: " + n);
    }
}

class Producer implements Runnable {
    Q q;

    Producer(Q q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }

    public void run() {
        int i = 0;
        while (true) {
            q.put(i++);
        }
    }
}

class Consumer implements Runnable {
    Q q;

    Consumer(Q q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }

    public void run() {
        while (true) {
            q.get();
        }
    }
}

class PC {
    public static void main(String args[]) {
        Q q = new Q();
        new Producer(q);
        new Consumer(q);

        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.exit(0);
    }
}

儘管Q類中的put( )和get( )方法是同步的,沒有東西阻止生產者超越消費者,也沒有東西阻止消費者消費一樣的序列兩次。這樣,你就獲得下面的錯誤輸出(輸出將隨處理器速度和裝載的任務而改變,每次也會不一樣):

Put: 0
Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7

生產者生成1後,消費者依次得到一樣的1五次。生產者在繼續生成2到7,消費者沒有機會得到它們。

用Java正確的編寫該程序是用wait( )和notify( )來對兩個方向進行標誌,以下所示:

// A correct implementation of a producer and consumer.
class Q {
    int n;
    boolean valueSet = false;

    synchronized int get() {
        if (!valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        System.out.println("Got: " + n);
        valueSet = false;
        notify();
        return n;
    }

    synchronized void put(int n) {
        if (valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        this.n = n;
        valueSet = true;
        System.out.println("Put: " + n);
        notify();
    }
}

內部get( ), wait( )被調用。這使執行掛起直到Producer 告知數據已經預備好。這時,內部get( ) 被恢復執行。獲取數據後,get( )調用notify( )。這告訴Producer能夠向序列中輸入更多數據。在put( )內,wait( )掛起執行直到Consumer取走了序列中的項目。當執行再繼續,下一個數據項目被放入序列,notify( )被調用,這通知Consumer它應該移走該數據。

下面是該程序的輸出,它清楚的顯示了同步行爲:

Put: 0
Got: 0
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3

死鎖

須要避免的與多任務處理有關的特殊錯誤類型是死鎖(deadlock)。死鎖發生在當兩個線程對一對同步對象有循環依賴關係時。例如,假定一個線程進入了對象X的管程而另外一個線程進入了對象Y的管程。若是X的線程試圖調用Y的同步方法,它將像預料的同樣被鎖定。而Y的線程一樣但願調用X的一些同步方法,線程永遠等待,由於爲到達X,必須釋放本身的Y的鎖定以使第一個線程能夠完成。死鎖是很難調試的錯誤,由於:

  • 一般,它極少發生,只有到兩線程的時間段恰好符合時才能發生。
  • 它可能包含多於兩個的線程和同步對象(也就是說,死鎖在比剛講述的例子有更多複雜的事件序列的時候能夠發生)。

爲充分理解死鎖,觀察它的行爲是頗有用的。下面的例子生成了兩個類,A和B,分別有foo( )和bar( )方法。這兩種方法在調用其餘類的方法前有一個短暫的停頓。主類,名爲Deadlock,建立了A和B的實例,而後啓動第二個線程去設置死鎖環境。foo( )和bar( )方法使用sleep( )強迫死鎖現象發生。

// An example of deadlock.
class A {
    synchronized void foo(B b) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " entered A.foo");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            System.out.println("A Interrupted");
        }
        System.out.println(name + " trying to call B.last()");
        b.last();
    }

    synchronized void last() {
        System.out.println("Inside A.last");
    }
}

class B {
    synchronized void bar(A a) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " entered B.bar");
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            System.out.println("B Interrupted");
        }
        System.out.println(name + " trying to call A.last()");
        a.last();
    }

    synchronized void last() {
        System.out.println("Inside A.last");
    }
}

class Deadlock implements Runnable {
    A a = new A();
    B b = new B();

    Deadlock() {
        Thread.currentThread().setName("MainThread");
        Thread t = new Thread(this, "RacingThread");
        t.start();
        a.foo(b); // get lock on a in this thread.
        System.out.println("Back in main thread");
    }

    public void run() {
        b.bar(a); // get lock on b in other thread.
        System.out.println("Back in other thread");
    }

    public static void main(String args[]) {
        new Deadlock();
    }
}

運行程序後,輸出以下:

MainThread entered A.foo
RacingThread entered B.bar
MainThread trying to call B.last()
RacingThread trying to call A.last()

由於程序死鎖,RacingThread在等待管程a時佔用管程b,同時,MainThread佔用a等待b。該程序永遠都不會結束。像該例闡明的,你的多線程程序常常被鎖定,死鎖是你首先應檢查的問題。


9. 掛起、恢復和終止線程

有時,線程的掛起是頗有用的。例如,一個獨立的線程能夠用來顯示當日的時間。若是用戶不但願用時鐘,線程被掛起。在任何情形下,掛起線程是很簡單的,一旦掛起,從新啓動線程也是一件簡單的事。

掛起,終止和恢復線程機制在Java 2和早期版本中有所不一樣。儘管你運用Java 2的途徑編寫代碼,你仍需瞭解這些操做在早期Java環境下是如何完成的。例如,你也許須要更新或維護老的代碼。你也須要了解爲何Java 2會有這樣的變化。由於這些緣由,下面內容描述了執行線程控制的原始方法,接着是Java 2的方法。

Java 1.1或更早版本的線程的掛起、恢復和終止

先於Java2的版本,程序用Thread 定義的suspend() 和 resume() 來暫停和再啓動線程。它們的形式以下:

final void suspend( )
final void resume( )

下面的程序描述了這些方法:

// Using suspend() and resume().
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;

    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        t.start(); // Start the thread
    }

    // This is the entry point for thread.
    public void run() {
        try {
            for (int i = 15; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(200);
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted.");
        }
        System.out.println(name + " exiting.");
    }
}

class SuspendResume {
    public static void main(String args[]) {
        NewThread ob1 = new NewThread("One");
        NewThread ob2 = new NewThread("Two");
        try {
            Thread.sleep(1000);
            ob1.t.suspend();
            System.out.println("Suspending thread One");
            Thread.sleep(1000);
            ob1.t.resume();
            System.out.println("Resuming thread One");
            ob2.t.suspend();
            System.out.println("Suspending thread Two");
            Thread.sleep(1000);
            ob2.t.resume();
            System.out.println("Resuming thread Two");
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        // wait for threads to finish
        try {
            System.out.println("Waiting for threads to finish.");
            ob1.t.join();
            ob2.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

程序輸出以下:

New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
One: 15
Two: 15
Two: 14
One: 14
Two: 13
One: 13
Two: 12
One: 12
Two: 11
One: 11
Suspending thread One
Two: 10
Two: 9
Two: 8
Two: 7
Two: 6
Resuming thread One
Suspending thread Two
One: 10
One: 9
One: 8
One: 7
One: 6
Resuming thread Two
Waiting for threads to finish.
Two: 5
One: 5
Two: 4
One: 4
Two: 3
One: 3
Two: 2
One: 2
Two: 1
One: 1
Two exiting.
One exiting.
Main thread exiting.

Thread類一樣定義了stop() 來終止線程。它的形式以下:

void stop( )

一旦線程被終止,它不能被resume() 恢復繼續運行。

Java 2中掛起、恢復和終止線程

Thread定義的suspend(),resume()和stop()方法看起來是管理線程的完美的和方便的方法,它們不能用於新Java版本的程序。下面是其中的緣由。Thread類的suspend()方法在Java2中不被同意,由於suspend()有時會形成嚴重的系統故障。假定對關鍵的數據結構的一個線程被鎖定的狀況,若是該線程在那裏掛起,這些鎖定的線程並無放棄對資源的控制。其餘的等待這些資源的線程可能死鎖。

Resume()方法一樣不被贊同。它不引發問題,但不能離開suspend()方法而獨立使用。Thread類的stop()方法一樣在Java 2中受到反對。這是由於該方法可能致使嚴重的系統故障。設想一個線程正在寫一個精密的重要的數據結構且僅完成一個零頭。若是該線程在此刻終止,則數據結構可能會停留在崩潰狀態。

由於在Java 2中不能使用suspend(),resume()和stop() 方法來控制線程,你也許會想那就沒有辦法來中止,恢復和結束線程。其實否則。相反,線程必須被設計以使run() 方法按期檢查以來斷定線程是否應該被掛起,恢復或終止它本身的執行。有表明性的,這由創建一個指示線程狀態的標誌變量來完成。只要該標誌設爲「running」,run()方法必須繼續讓線程執行。若是標誌爲「suspend」,線程必須暫停。若設爲「stop」,線程必須終止。

固然,編寫這樣的代碼有不少方法,但中心主題對全部的程序應該是相同的。

下面的例題闡述了從Object繼承的wait()和notify()方法怎樣控制線程的執行。該例與前面講過的程序很像。然而,不被贊同的方法都沒有用到。讓咱們思考程序的執行。

NewTread 類包含了用來控制線程執行的布爾型的實例變量suspendFlag。它被構造函數初始化爲false。Run()方法包含一個監測suspendFlag 的同步聲明的塊。若是變量是true,wait()方法被調用以掛起線程。Mysuspend()方法設置suspendFlag爲true。Myresume()方法設置suspendFlag爲false而且調用notify()方法來喚起線程。最後,main()方法被修改以調用mysuspend()和myresume()方法。

// Suspending and resuming a thread for Java2
class NewThread implements Runnable {
    String name; // name of thread
    Thread t;
    boolean suspendFlag;

    NewThread(String threadname) {
        name = threadname;
        t = new Thread(this, name);
        System.out.println("New thread: " + t);
        suspendFlag = false;
        t.start(); // Start the thread
    }

    // This is the entry point for thread.
    public void run() {
        try {
            for (int i = 15; i > 0; i--) {
                System.out.println(name + ": " + i);
                Thread.sleep(200);
                synchronized (this) {
                    while (suspendFlag) {
                        wait();
                    }
                }
            }
        } catch (InterruptedException e) {
            System.out.println(name + " interrupted.");
        }
        System.out.println(name + " exiting.");
    }

    void mysuspend() {
        suspendFlag = true;
    }

    synchronized void myresume() {
        suspendFlag = false;
        notify();
    }
}

class SuspendResume {
    public static void main(String args[]) {
        NewThread ob1 = new NewThread("One");
        NewThread ob2 = new NewThread("Two");
        try {
            Thread.sleep(1000);
            ob1.mysuspend();
            System.out.println("Suspending thread One");
            Thread.sleep(1000);
            ob1.myresume();
            System.out.println("Resuming thread One");
            ob2.mysuspend();
            System.out.println("Suspending thread Two");
            Thread.sleep(1000);
            ob2.myresume();
            System.out.println("Resuming thread Two");
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        // wait for threads to finish
        try {
            System.out.println("Waiting for threads to finish.");
            ob1.t.join();
            ob2.t.join();
        } catch (InterruptedException e) {
            System.out.println("Main thread Interrupted");
        }
        System.out.println("Main thread exiting.");
    }
}

該程序的輸出與前面的程序相同。儘管Java 2控制線程機制不像老方法那樣「乾淨」,然而,它是確保運行時不發生錯誤的方法。它是全部新的代碼必須採用的方法。


10. 使用多線程機制

若是你和大多數程序員同樣,那麼在語言中加入多線程支持對你來講是很新鮮的事物。有效運用這種支持的關鍵是併發思考而不是連續思考。例如,當你的程序有兩個能夠並行執行的子系統,建立他們各自的線程。仔細的運用多線程,你能編寫出很是有效的程序。然而要注意:若是你建立太多的線程,你可能會減弱而不是增強程序的性能。記住,上下文轉換是須要開銷的。若是你建立了太多的線程,更多的CPU時間會用於上下文轉換而不是用來執行程序。

Written with StackEdit.

相關文章
相關標籤/搜索