歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網

Java線程

日期:2017/2/27 15:48:40   编辑:Linux教程

線程是一個單獨程序流程。多線程是指一個程序可以同時運行多個任務,每個任務由一個單獨的線程來完成。也就是說,多個線程可以同時在一個程序中運 行,並且每一個線程完成不同的任務。程序可以通過控制線程來控制程序的運行,例如線程的等待、休眠、喚起線程等。本章將向讀者介紹線程的機制、如何操作和 使用線程以及多線程編程。

1. 線程的基本知識

線程是程序運行的基本單位,一個程序中可以同時運行多個線程。如果程序被設置為多線程, 可以提高程序運行的效率和處理速度。 Java中線程的實現通常有兩種方法: 派生 Thread類和實現 Runnable接口。本節主要講述線程的概念和創建線程的方法。

1. 什麼是線程

傳統的程序設計語言同一時刻只能執行單任務操作,效率非常低,如果網絡程序在接收數據時發生阻塞,只能等到程序接收數據之後才能繼續運行。隨著 Internet 的飛速發展,這種單任務運行的狀況越來越不被接受。如果網絡接收數據阻塞,後台服務程序就會一直處於等待狀態而不能繼續任何操作。 這種阻塞情況經常發生, 這時的 CPU資源完全處於閒置狀態。 多線程實現後台服務程序可以同時處理多個任務,並不發生阻塞現象。多線程是 Java 語言的一個很重要的特征。 多線程程序設計最大的特點就是能夠提高程序執行效率和處理速度。Java 程序可同時並行運行多個相對獨立的線程。例如創建一個線程來接收數據,另一個線程發送數據,既使發送線程在接收數據時被阻塞,接受數據線程仍然可以運 行。 線程(Thread)是控制線程(Thread of Control)的縮寫,它是具有一定順序的指令序列(即所編寫的程序代碼)、存放方法中定義局部變量的棧和一些共享數據。線程是相互獨立的,每個方法的 局部變量和其他線程的局部變量是分開的,因此,任何線程都不能訪問除自身之外的其他線程的局部變量。如果兩個線程同時訪問同一個方法,那每個線程將各自得 到此方法的一個拷貝。 Java 提供的多線程機制使一個程序可同時執行多個任務。線程有時也被稱為小進程,它是從一個大進程裡分離出來的小的獨立的線程。由於實現了多線程技術,Java 顯得更健壯。多線程帶來的好處是更好的交互性能和實時控制性能。多線程是強大而靈巧的編程工具,但要用好它卻不是件容易的事。在多線程編程中,每個線程都 通過代碼實現線程的行為,並將數據供給代碼操作。編碼和數據有時是相當獨立的,可分別向線程提供。多個線程可以同時處理同一代碼和同一數據,不同的線程也 可以處理各自不同的編碼和數據。

2 .創建線程方法

Java程序都是聲明一個公共類,並在類內實現一個 main 方法。事實上,這些程序就是一個單線程程序。當它執行完main 方法的程序後,線程正好退出,程序同時結束運行。
public class OnlyThread {  
    public static void main(String args[]) {  
        run(); // 調用靜態run()方法  
    }  
  
    /** 
     * 實現run()方法 
     */  
    public static void run() {  
        // 循環計算輸出的*數目  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) { // 循環輸出指定的count數目的*  
                System.out.print('*');   
            }  
            System.out.println();  
        }  
    }  
}  

這只是建立了一個單一線程並執行的普通小程序,並沒有涉及到多線程的概念。

在 Java程序中,有兩種方法創建線程:
一是對 Thread 類進行派生並覆蓋 run方法;

二是通過實現 runnable接口創建。

3. Thread 創建線程

在程序中創建新的線程的方法之一是繼承 Thread 類, 並通過 Thread子類聲明線程對象。繼承Thread 類並覆蓋 Thread類的 run 方法完成線程類的聲明, 通過new創建派生線程類的線程對象。run 中的代碼實現了線程的行為。

java.lang.Thread 類是一個通用的線程類,由於默認情況下 run 方法是空的,直接通過 Thread類實例化的線程對象不能完成任何事,所以可以通過派生 Thread 類,並用具體程序代碼覆蓋Thread 類中的 run 方法,實現具有各種不同功能的線程類。

1) Thread 創建線程步驟:

(1)創建一個新的線程類,繼承 Thread 類並覆蓋 Thread 類的 run()方法。
class ThreadType extends Thread{   
     public void run(){   
         ……   
     }   
}   
(2)創建一個線程類的對象,創建方法與一般對象的創建相同,使用關鍵字new完成。
ThreadType  tt = new ThreadType();   
(3)啟動新線程對象,調用 start()方法。
tt.start();   
(4)線程自己調用 run()方法。
void run();   

2) Thread創建一個線程

下面是通過Thread創建線程的例子:產生一個新的線程

class ThreadDemo1 extends Thread {  
    ThreadDemo1() {  
    }  
  
    // 聲明ThreadDemo1帶參數的構造方法  
    ThreadDemo1(String szName) {  
        super(szName);  
    }  
  
    // 重載run函數  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) {// 循環輸出指定的count數目的*  
                System.out.print('*');  
            }  
            System.out.println();  
        }  
    }  
  
    public static void main(String argv[]) {  
        ThreadDemo1 td = new ThreadDemo1(); // 創建,並初始化ThreadDemo1類型對象td  
        td.start(); // 調用start()方法執行一個新的線程  
    }  
}  
OnlyThread.java程序與程序ThreadDemo1.java表面上看運行結果相同,但是仔細對照會發現,程序OnlyThread.java中對 run方法的調用在程序ThreadDemo1.java中變成了對 start 方法的調用,並且程序ThreadDemo1.java確派生 Thread類,創建新的線程類。

3) Thread創建多個線程

//文件:程序10.3 ThreadDemo2.java 描述:產生三個新的線程
public class ThreadDemo2 extends Thread {  
    // 聲明無參數,空構造方法  
    ThreadDemo2() {  
    }  
  
    // 聲明帶有字符串參數的構造方法  
    ThreadDemo2(String szName) {  
        super(szName); // 調用父類的構造方法  
    }  
  
    // 重載run函數  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) {// 循環輸出指定的count數目的*  
                System.out.print('*');  
            }  
            System.out.println();  
        }  
    }  
  
    public static void main(String argv[]) {  
        ThreadDemo2 td1 = new ThreadDemo2(); // 創建,並初始化ThreadDemo2類型對象td1  
        ThreadDemo2 td2 = new ThreadDemo2(); // 創建,並初始化ThreadDemo2類型對象td2  
        ThreadDemo2 td3 = new ThreadDemo2(); // 創建,並初始化ThreadDemo2類型對象td3  
        td1.start(); // 啟動線程td1  
        td2.start(); // 啟動線程td2  
        td3.start(); // 啟動線程td3  
    }  
}  

創建了 3 個線程 td1、td2、td3,它們分別執行自己的 run方法。在實際中運行的結果並不是想要的直角三角形, 而是一些亂七八糟的 “*” 行,長短並沒有一定的規律,這是因為線程並沒有按照程序中調用的順序來執行, 而是產生了多個線程賽跑現象。 運行結果:

注意:Java線程並不能按調用順序執行,而是並行執行的單獨代碼。如果要想得到完整的直角三角形,需要在執行一個線程之前,判斷程序前面的線程是否終止,如果已經終止,再來調用該線程。

4. Runnable 接口創建線程

通過實現 Runnable 接口的方法是創建線程類的第二種方法。利用實現 Runnable 接口來創建線程的方法可以解決 Java 語言不支持的多重繼承問題。 Runnable 接口提供了 run()方法的原型,因此創建新的線程類時,只要實現此接口,即只要特定的程序代碼實現Runnable接口中的 run()方法,就可完成新線程類的運行。

擴展Thread類創建線程的方式,適合編寫簡單的應用程序代碼,而實現Runnable接口創建線程,能夠避免Java單繼承的局限,適合同一代碼的多線程處理同一資源的情況,代碼具有良好的一致性,是更符合面向對象思想的設計方式。

1) Runnable 創建線程步驟

(1)創建一個實現 Runnable 接口的類,並且在這個類中重寫 run 方法。
class ThreadType implements Runnable{   
     public void run(){   
         ……   
     }   
}   
(2)使用關鍵字 new新建一個 ThreadType 的實例。
Runnable rb = new ThreadType ();   
(3)通過 Runnable 的實例創建一個線程對象,在創建線程對象時,調用的構造函數是new Thread(ThreadType),它用 ThreadType 中實現的 run()方法作為新線程對象的 run()方法。
Thread td = new Thread(rb);   
(4)通過調用 ThreadType 對象的 start()方法啟動線程運行。
td.start();   

2) Runnable 創建線程

class ThreadDemo3 implements Runnable {  
    // 重載run函數  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++){ // 循環計算輸出的*數目  
            for (int i = 0; i < count; i++){ // 循環輸出指定的count數目的*  
                System.out.print('*');   
            }  
            System.out.println();   
        }  
    }  
  
    public static void main(String argv[]) {  
        Runnable rb = new ThreadDemo3(); // 創建,並初始化ThreadDemo3對象rb  
        Thread td = new Thread(rb); // 通過Thread創建線程  
        td.start(); // 啟動線程td  
    }  
}  

2. 線程的狀態

線程的整個周期由線程創建、可運行狀態、不可運行狀態和退出等部分組成,這些狀態之間的轉化是通過線程提供的一些方法完成的。

1.線程周期

一個線程有4 種狀態,任何一個線程都處於這4種狀態中的一種狀態:
1) 創建(new)狀態:調用 new方法產生一個線程對象後、調用 start 方法前所處的狀態。線程對象雖然已經創建,但還沒有調用 start 方法啟動,因此無法執行。當線程處於創建狀態時,線程對象可以調用 start 方法進入啟動狀態,也可以調用 stop 方法進入停止狀態。
2)可運行(runnable)狀態:當線程對象執行 start()方法後,線程就轉到可運行狀態。進入此狀態只是說明線程對象具有了可以運行的條件,但線程並不一定處於運行狀態。因為在單處理器系統中運行 多線程程序時,一個時間點只有一個線程運行,系統通過調度機制實現宏觀意義上的運行線程共享處理器。 因此一個線程是否在運行,除了線程必須處於 Runnable 狀態之外,還取決於優先級和調度。
3)不可運行(non Runnable)狀態:線程處於不可運行狀態是由於線程被掛起或者發生阻塞,例如對一個線程調用 wait()函數後,它就可能進入阻塞狀態;調用線程的notify或notifyAll 方法後它才能再次回到可執行狀態。
4)退出(done)狀態:一個線程可以從任何一個狀態中調用 stop 方法進入退出狀態。線程一旦進入退出狀態就不存在了,不能再返回到其他的狀態。除此之外,如果線程執行完 run方法,也會自動進入退出狀態。
創建狀態、可運行狀態、不可運行狀態、退出狀態之間的轉換關系如圖 所示。

通過 new第一次創建線程時,線程位於創建狀態,這時不能運行線程,只能等待進一步的方法調用改變其狀態。然後,線程通過調用 start方法啟動
線程,並進入可執行狀態,或者調用方法 stop進入退出狀態。當程序位於退出狀態時,線程已經結束執行,這是線程的最後一個狀態,並且不能轉化到其他狀態。當程序的所有線程位於退出狀態時,程 序會強行終止。當線程位於可執行狀態時,在一個特定的時間點上,每一個系統處理器只能運行一個線程。 此時如果線程被掛起,執行就會被中斷或者進入休眠狀態,那麼線程將進入不可執行狀態,並且不可執行狀態可以通過 resume、notify等方法返回到可執行狀態。表10-1列舉了線程狀態轉換的函數。

線程狀態轉換函數:
方法 描述 有效狀態 目的狀態
start() 開始執行一個線程 New Runnable
stop() 結束執行一個線程 New或Runnable Done
sleep(long) 暫停一段時間,這個時間為給定的毫秒 Runnable NonRunnable
sleep(long,int) 暫停片刻,可以精確到納秒 Runnable NonRunnable
suspend() 掛起執行 Runnable NonRunnable
resume() 恢復執行 NonRunnable Runnable
yield() 明確放棄執行 Runnable Runnable
wait() 進入阻塞狀態 Runnable NonRunnable
notify() 阻塞狀態解除 NonRunnable Runnable
注意:stop()、suspend()和 resume()方法現在已經不提倡使用,這些方法在虛擬機中可能引起“死鎖”現象。suspend()和 resume()方法的替代方法是 wait()和 sleep()。線程的退出通常采用自然終止的方法,建議不要人工調用 stop()方法。

2 線程的創建和啟動

Java 是面向對象的程序設計語言,設計的重點就是類的設計與實現。Java 利用線程類Thread 來創建線程,線程的創建與普通類對象的創建操作相同。Java通過線程類的構造方法創建一個線程,並通過調用 start 方法啟動該線程。
實際上,啟動線程的目的就是為了執行它的 run()方法,而 Thread 類中默認的 run()方法沒有任何可操作代碼,所以用 Thread類創建的線程不能完成任何任務。為了讓創建的線程完成特定的任務,必須重新定義 run()方法。在第一節中已經講述過,Java 通常有兩種重新定義run()方法的方式:

1)派生線程類 Thread 的子類,並在子類中重寫 run()方法:Thread 子類的實例對象是一個線程對象,並且該線程有專門定制的線程 run()方法,啟動線程後就執行子類中重寫的 run()方法。
2)實現 Runnable 接口並重新定義 run()方法:先定義一個實現 Runnable()接口的類,在該類中定義 run()方法,然後創建新的線程類對象,並以該對象作為 Thread 類構造方法的參數創建一個線程。

注意:調用線程的 run()方法是通過啟動線程的start()方法來實現的。 因為線程在調用start()方法之後,系統會自動調用 run()方法。與一般方法調用不同的地方在於一般方法調用另外一個方法後,必須等被調用的方法執行完畢才能返回,而線程的 start()方法被調用之後,系統會得知線程准備完畢並且可以執行run()方法,start()方法就返回了,start()方法不會等待run() 方法執行完畢。

3. 線程狀態轉換

1.線程進入可執行狀態
當以下幾種情況發生時,線程進入可執行狀態。
(1)其他線程調用notify()或者 notifyAll()方法,喚起處於不可執行狀態的線程。
public final void notify()
public final void notifyAll()
notify 僅僅喚醒一個線程並允許它獲得鎖,notifyAll 喚醒所有等待這個對象的線程,並允許它們獲得鎖。
(2)線程調用 sleep(millis)方法,millis毫秒之後線程會進入可執行狀態。
static void sleep(long millis) throws InterruptedException 在 millis 毫秒數內讓當前正在執行的線程進入休眠狀態,等到時間過後,該線程會自動蘇醒並繼續執行。sleep方法的精確度受到系統計數器的影響。
static void sleep(long millis, int nanos) throws InterruptedException 在毫秒數(millis)加納秒數(nanos)內讓當前正在執行的線程進入休眠狀態,此操作的精確度也受到 系統計數器的影響。

(3)線程對I/O操作的完成。

2.線程進入不可執行狀態
當以下幾種情況發生時,線程進入不可執行狀態。
(1)線程自動調用 wait()方法,等待某種條件的發生。
public final void wait() throws InterruptedException
當其他線程調用 notify()方法或 notifyAl()方法後,處於等待狀態的線程獲得鎖之後才會被喚醒,然後該線程一直等待重新獲得對象鎖才繼續運行。
(2)線程調用 sleep()方法進入不可執行狀態,在一定時間後會進入可執行狀態。
(3)線程等待 I/O操作的完成。

線程阻塞的例子。

public class  ThreadSleep   
{   
 public static void main(String[ ] args)    
 {   
  SubThread st = new SubThread("SubThread");  //創建,並初始化SubThread 對象st   
  st.start();         //啟動線程st   
 }   
}   
   
class SubThread extends Thread{   
 SubThread(){}        //聲明,實現SubThread無參數構造方法   
 //聲明,實現SubThread帶字符串參數構造方法   
 SubThread(String Name)   
 {   
  super(Name);        //調用父類的構造方法   
 }   
 //重載run函數   
 public void run()   
 {   
  for (int count = 1,row = 1; row < 10; row++,count++) //循環計算輸出的*數目   
  {   
   for (int i = 0; i < count; i++)      //循環輸出指定的count數目的*   
   {   
    System.out.print('*');     //輸出*   
   }   
   try         //try-catch塊,用於捕獲異常   
   {     Thread.sleep(1000);     //線程休眠1秒鐘   
    System.out.print("\t wait........");   
   }   
   catch (InterruptedException e)    //捕獲異常InterruptedException   
   {   
    e.printStackTrace();     //異常拋出信息   
   }   
   System.out.println();      //輸出換行符   
  }   
 }   
}   

程序ThreadSleep 中,每輸出一行*就要休息1 秒鐘。當執行 sleep()語句後, 線程進入不可執行狀態等待1 秒鐘之後,線程 st 會自動蘇醒並繼續執行。由於 slee

方法拋出 InterruptedException異常, 所以在調用時必須捕獲異常。

4 .等待線程結束

isAlive()方法用來判斷一個線程是否存活。當線程處於可執行狀態或不可執行狀態時,isAlive()方法返回 true; 當線程處於創建狀態或退出狀態時, 則返回 false。 也就是說, isAlive()方法如果返回 true,並不能判斷線程是處於可運行狀態還是不可運行狀態。isAlive()方法的原型如下所示。
public final boolean isAlive()

該方法用於測試線程是否處於活動狀態。活動狀態是指線程已經啟動(調用 start方法)且尚未退出所處的狀態,包括可運行狀態和不可運行狀態。可以通過該方法解決程序 10.3中的問題,先判斷第一個線程是否已經終止,如果終止再來調用第二個線程。這裡提供兩種方法:

第一種方法是不斷查詢第一個線程是否已經終止,如果沒有,則讓主線程睡眠一直到它終止即“while/isAlive/sleep”,格式如下。

線程1.start();   
while(線程 1.isAlive()) {   
 Thread.sleep(休眠時間);   
}   
線程2.start();  
第二種是利用 join()方法。
1)public final void join(long millis) throws InterruptedException 等待該線程終止的時間最長為毫秒(millis),超時為0 意味著要一直等下去。
2) public final void join(long millis,int nanos) throws InterruptedException 等待該線程終止的時間最長為毫秒(millis)加納秒(nanos)。
3)public final void join() throws InterruptedException 等待該線程終止。
等待線程結束並執行另外一個線程的例子。 該例子 等待一個線程的結束的兩種方法 :
package Test;  
  
class WaitThreadStop extends Thread {  
    // 聲明,並實現WaitThreadStop無參數構造方法  
    WaitThreadStop() {  
    }  
  
    // 聲明,並實現帶有一個字符串參數的構造方法  
    WaitThreadStop(String szName) {  
        super(szName); // 調用父類的構造方法  
    }  
  
    // 重載run函數  
    public void run() {  
        for (int count = 1, row = 1; row < 10; row++, count++) {  
            for (int i = 0; i < count; i++) {  
                System.out.print('*'); // 輸出*  
            }  
            System.out.println(); // 輸出換行符  
        }  
    }  
}  
  
public class WaitThreadStopMain {  
    public static void main(String argv[ ]){   
      WaitThreadStopMain test = new WaitThreadStopMain();    //創建,初始化WaitThreadStopMain對象test   
      test.Method1();  //調用Method1方法   
      //test.Method2();   
     }  
    // 第一種方法:while/isAlive/sleep  
    public void Method1() {  
        WaitThreadStop th1 = new WaitThreadStop(); // 創建,並初始化WaitThreadStop對象th1  
        WaitThreadStop th2 = new WaitThreadStop(); // 創建,並初始化WaitThreadStop對象th2  
        // 執行第一個線程  
        th1.start();  
        // 查詢第一個線程的狀態  
        while (th1.isAlive()) {  
            try {  
                Thread.sleep(100); // 休眠100毫秒  
            } catch (InterruptedException e) {  
                e.printStackTrace(); // 異常信息輸出  
            }  
        }  
        // 當第一個線程終止後,運行第二個線程  
        th2.start(); // 啟動線程th2  
    }  
  
    // 第二種方法,使用join方法實現等待其他線程結束  
    public void Method2() {  
        WaitThreadStop th1 = new WaitThreadStop(); // 創建, 並初始化WaitThreadStop對象th1  
        WaitThreadStop th2 = new WaitThreadStop(); // 創建,並初始化WaitThreadStop對象th2  
        // 執行第一個線程  
        th1.start();  
        try {  
            th1.join(); // th1調用join 方法  
        } catch (InterruptedException e) {  
            e.printStackTrace(); // 異常信息輸出  
        }  
        // 執行第二個線程  
        th2.start();  
    }  
}  

3. 線程調度

多線程應用程序的每一個線程的重要性和優先級可能不同,例如有多個線程都在等待獲得CPU的時間片, 那麼優先級高的線程就能搶占CPU並得以執行; 當多個線程交替搶占CPU時,優先級高的線程占用的時間應該多。因此,高優先級的線程執行的效率會高些,執行速度也會快些。

在 Java 中,CPU的使用通常是搶占式調度模式不需要時間片分配進程。搶占式調度模式是指許多線程同時處於可運行狀態,但只有一個線程正在運行。當線程一直運行直 到結束,或者進入不可運行狀態,或者具有更高優先級的線程變為可運行狀態,它將會讓出 CPU。線程與優先級相關的方法如下:

public final void setPriority(int newPriority) 設置線程的優先級為 newPriority :

newPriority 的值必須在 MIN_PRIORITY 到MAX_PRIORITY范圍內,通常它們的值分別是1和10。目前Windows系統只支持3個級別的優

先級, 它們分別是Thread.MAX_PRIORITY、 Thread.MIN_PRIORITY和Thread.NORM_PRIORITY。

public final int getPriority() 獲得當前線程的優先級。

線程優先級的例子:

class  InheritThread extends Thread {   
    //自定義線程的run()方法   
    public void run(){   
         System.out.println("InheritThread is running…"); //輸出字符串信息   
         for(int i=0;i<10;i++){   
              System.out.println(" InheritThread: i="+i);  //輸出信息   
              try{   
                  Thread.sleep((int)Math.random()*1000); //線程休眠   
             }   
             catch(InterruptedException e)     //捕獲異常   
             {}   
        }   
    }   
}   
通過Runnable接口創建的另外一個線程 :
class RunnableThread implements Runnable {  
    // 自定義線程的run()方法  
    public void run() {  
        System.out.println("RunnableThread is running…"); // 輸出字符串信息  
        for (int i = 0; i < 10; i++) {  
            System.out.println("RunnableThread : i=" + i); // 輸出i  
            try {  
                Thread.sleep((int) Math.random() * 1000); // 線程休眠  
            } catch (InterruptedException e) { // 捕獲異常  
            }  
        }  
    }  
}  
  
public class ThreadPriority {  
    public static void main(String args[]) {  
        // 用Thread類的子類創建線程  
        InheritThread itd = new InheritThread();  
        // 用Runnable接口類的對象創建線程  
        Thread rtd = new Thread(new RunnableThread());  
        itd.setPriority(5); // 設置myThread1的優先級5  
        rtd.setPriority(5); // 設置myThread2的優先級5  
        itd.start(); // 啟動線程itd  
        rtd.start(); // 啟動線程rtd  
    }  
}  
在程序ThreadPriority.java中,線程 rtd 和 itd 具有相同的優先級,所以它們交互占用 CPU,宏觀上處於並行運行狀態。結果如圖3.

重新設定優先級:

itd.setPriority(1); //設置myThread1的優先級1
rtd.setPriority(10); //設置myThread2的優先級10
運行程序結果如圖4所示。

圖3相同優先級 圖2 不同的優先級

從運行結構可以看出程序ThreadPriority.java修改後,由於設置了線程itd和 rtd 的優先級,並且 rtd的優先級較高,基本上是 rtd都優先搶占 CPU資源。

4. 線程同步

Java 應用程序中的多線程可以共享資源,例如文件、數據庫、內存等。當線程以並發模式訪問共享數據時,共享數據可能會發生沖突。Java引入線程同步的概念,以 實現共享數據的一致性。線程同步機制讓多個線程有序的訪問共享資源,而不是同時操作共享資源。

1 . 同步概念

在線程異步模式的情況下,同一時刻有一個線程在修改共享數據,另一個線程在讀取共享數據,當修改共享數據的線程沒有處理完畢,讀取數據的線程肯定會得到錯誤的結果。如果采用多線程的同步控制機制,當處理共享數據的線程完成處理數據之後,讀取線程讀取數據。
通過分析多線程出售火車票的例子,可以更好得理解線程同步的概念。線程 Thread1 和線程 Thread2 都可以出售火車票,但是這個過程中會出現數據與時間信息不一致的情況。線程 Thread1 查詢數據庫,發現某張火車票 T 可以出售,所以准備出售此票;此時系統切換到線程Thread2執行, 它在數據庫中查詢存票, 發現上面的火車票T可以出售, 所以線程Thread2將這張火車票 T 售出;當系統再次切換到線程 Thread1 執行時,它又賣出同樣的票 T。這是一個典型的由於數據不同步而導致的錯誤。
下面舉一個線程異步模式訪問數據的例子。
//文件:程序ThreadNoSynchronized.java   描述:多線程不同步的原因   
class ShareData {  
    public static String szData = ""; // 聲明,並初始化字符串數據域,作為共享數據  
  
}  
class ThreadDemo extends Thread {  
    private ShareData oShare; // 聲明,並初始化ShareData 數據域  
    ThreadDemo() {  
    } // 聲明,並實現ThreadDemo 構造方法  
  
    // 聲明,並實現ThreadDemo 帶參數的構造方法  
    ThreadDemo(String szName, ShareData oShare) {  
        super(szName); // 調用父類的構造方法  
        this.oShare = oShare; // 初始化oShare域  
    }  
    public void run() {  
        for (int i = 0; i < 5; i++) {  
            if (this.getName().equals("Thread1")) {  
                oShare.szData = "這是第 1 個線程";  
                // 為了演示產生的問題,這裡設置一次睡眠  
                try {  
                    Thread.sleep((int) Math.random() * 100); // 休眠  
                } catch (InterruptedException e) { // 捕獲異常  
                }  
                System.out.println(this.getName() + ":" + oShare.szData); // 輸出字符串信息  
            } else if (this.getName().equals("Thread2")) {  
                oShare.szData = "這是第 2 個線程";  
                // 為了演示產生的問題,這裡設置一次睡眠  
                try {  
                    Thread.sleep((int) Math.random() * 100); // 線程休眠  
                } catch (InterruptedException e) // 捕獲異常  
                {  
                }  
                System.out.println(this.getName() + ":" + oShare.szData); // 輸出字符串信息  
            }  
        }  
    }  
}  
  
public class ThreadNoSynchronized {  
    public static void main(String argv[]) {  
        ShareData oShare = new ShareData(); // 創建,初始化ShareData對象oShare  
        ThreadDemo th1 = new ThreadDemo("Thread1", oShare); // 創建線程th1  
        ThreadDemo th2 = new ThreadDemo("Thread2", oShare); // 創建線程th2  
        th1.start(); // 啟動線程th1  
        th2.start(); // 啟動線程th2  
    }  
}  

運行結果如下:

Thread1:這是第 2 個線程
Thread1:這是第 1 個線程
Thread1:這是第 1 個線程
Thread1:這是第 1 個線程
Thread1:這是第 1 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程
Thread2:這是第 2 個線程

程序中預想的結果是:“Thead1:這是第1 個線程”或“Thead2:這是第2 個線程”,但是線程對數據的異步操作導致運行結果出現了差錯。 上面程序是由於線程不同步而導致錯誤。 為了解決此類問題,Java 提供了“鎖”機制實現線程的同步。

鎖機制的原理是每個線程進入共享代碼之前獲得鎖,否則不能進入共享代碼區,並且在退出共享代碼之前釋放該鎖,這樣就解決了多個線程競爭共享代碼的情況,達 到線程同步的目的。Java中鎖機制的實現方法是共享代碼之前加入 synchronized 關鍵字。

在一個類中,用關鍵字 synchonized 聲明的方法為同步方法。Java 有一個專門負責管理線程對象中同步方法訪問的工具——同步模型監視器,它的原理是為每個具有同步代碼的對象准備惟一的一把“鎖”。當多個線程訪問對象時, 只有取得鎖的線程才能進入同步方法,其他訪問共享對象的線程停留在對象中等待,如果獲得鎖的線程調用wait方法放棄鎖,那麼其他等待獲得鎖的線程將有機 會獲得鎖。當某一個等待線程取得鎖,它將執行同步方法,而其他沒有取得鎖的線程仍然繼續等待獲得鎖。
Java 程序中線程之間通過消息實現相互通信,wait()、notify()及 notifyAll()方法可完成線程間的消息傳遞。例如,一個對象包含一個 synchonized 同步方法,同一時刻只能有一個獲得鎖的線程訪問該對象中的同步方法, 其他線程被阻塞在對象中等待獲得鎖。 當線程調用 wait()方法可使該線程進入阻塞狀態,其他線程調用notify()或 notifyAll()方法可以喚醒該線程。

2 .同步格式

當把一語句塊聲明為 synchornized,在同一時間,它的訪問線程之一才能執行該語句塊。

1) 方法同步:用關鍵字 synchonized 可將方法聲明為同步,格式如下。

class 類名{   
     public synchonized 類型名稱 方法名稱(){   
           ......   
     }   
}  
2)語句塊同步: 對於同步塊,synchornized 獲取的是參數中的對象鎖。
synchornized(obj)   
{    
  //………………….    
}   
當線程執行到這裡的同步塊時,它必須獲取 obj 這個對象的鎖才能執行同步塊;否則線程只能等待獲得鎖。必須注意的是obj對象的作用范圍不同,控制情況不盡相同。示例如下。
public void method()   
{    
  Object obj= new Object(); //創建局部Object類型對象obj   
  synchornized(obj)   //同步塊   
  {   
      //……………..    
  }    
}    

上面的代碼創建了一個局部對象obj。由於每一個線程執行到 Object obj = new Object()時都會產生一個 obj 對象,每一個線程都可以獲得創建的新的 obj對象的鎖,不會相互影響,因此這段程序不會起到同步作用。

3)同步類的屬性:如果同步的是類的屬性,情況就不同了。同步類的成員變量的一般格式如下。

class method   
{    
    Object o = new Object();  //創建Object類型的成員變量o   
public void test()   
{    
synchornized(o)  //同步塊   
{    
            //………………………   
        }    
    }    
}   

當兩個並發線程訪問同一個對象的 synchornized(o)同步代碼塊時,一段時間內只能有一個線程運行。另外的線程必須等到當前線程執行完同步代碼塊釋放鎖之後,獲得鎖的線程將執行同步代碼塊。

有時可以通過下面的格式聲明同步塊。

public void method()   
{    
synchornized(this)  //同步塊   
{    
    //………………………   
    }    
}   
當有一個線程訪問某個對象的 synchornized(this)同步代碼塊時,另外一個線程必須等待該線程執行完此代碼塊,其他線程可以訪問該對象中的非 synchornized(this)同步代碼。如果類中包含多個 synchornized(this)同步代碼塊,如果同步線程有一個訪問其中一個代碼塊,則其他線程不能訪問該對象的所有 synchornized(this)同步代碼塊。對於下面形式的同步塊而言,調用 ClassName 對象實例的並行線程中只有一個線程能夠訪問該對象。
synchornized(ClassName.class)   
{    
    //…………………….   
}   

5. 線程通信

1. 生產者與消費者

生產者與消費者是個很好的線程通信的例子,生產者在一個循環中不斷生產共享數據,而消費者則不斷消費生產者生產的共享數據。程序必須保證有共享數據,如果沒有,消費者必須等待生產新的共享數據。兩者之間的數據關系如下:
1) 生產者生產前,如果共享數據沒有被消費,則生產等待;生產者生產後,通知消費者消費。
2)消費者消費前,如果共享數據已經被消費完,則消費者等待;消費者消費後,通知生產者生產。
為了解決生產者和消費者的矛盾,引入了等待/通知(wait/notify)機制。

class Producer extends Thread {  
    Queue q;  
  
    Producer(Queue q) {  
        this.q = q;  
    }  
    public void run() {  
        for (int i = 1; i < 5; i++) {  
            q.put(i);  
        }  
    }  
}  
  
class Consumer extends Thread {  
    Queue q; // 聲明隊列q  
    Consumer(Queue q){   
        this.q = q; // 隊列q初始化  
    }  
    public void run() {  
        while (true) {// 循環消費元素  
            q.get(); // 獲取隊列中的元素  
        }  
    }  
}  

Producer 是一個生產者類,該生產者類提供一個以共享隊列作為參數的構造方法,它的run 方法循環產生新的元素,並將元素添加於共享隊列;Consumer 是一個消費者類,該消費者類提供一個以共享隊列作為參數的構造方法,它的 run 方法循環消費元素,並將元素從共享隊列刪除。

2.共享隊列

共享隊列類是用於保存生產者生產、消費者消費的共享數據。共享隊列有兩個域:value(元素的數目)、isEmpty(隊列的狀態)。共享隊列提供了put和 get 兩個方法。
class Queue {  
    int value = 0; // 聲明,並初始化整數類型數據域value  
    boolean isEmpty = true; // 聲明,並初始化布爾類型數據域isEmpty,用於判斷隊列的狀態  
  
    // 生產者生產方法  
    public synchronized void put(int v) {  
        // 如果共享數據沒有被消費,則生產者等待  
        if (!isEmpty) {  
            try {  
                System.out.println("生產者等待");  
                wait(); // 進入等待狀態  
            } catch (Exception e) // 捕獲異常  
            {  
                e.printStackTrace(); // 異常信息輸出  
            }  
        }  
        value += v; // value值加v  
        isEmpty = false; // isEmpty賦值為false  
        System.out.println("生產者共生產數量:" + v);  
        notify();  
    }  
  
    public synchronized int get() {  
        if (isEmpty) {  
            try {  
                System.out.println("消費者等待");  
                wait();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
        value--;  
        if (value < 1) {  
  
            isEmpty = true;  
        }  
        System.out.println("消費者消費一個,剩余:" + value);  
        notify();  
        return value;  
    }  
}  

生產者調用put方法生產共享數據,如果共享數據不為空,生產者線程進入等待狀態;否則將生成新的數據,然後調用notify方法喚醒消費者線程進行消費;
消費者調用get方法消費共享數據,如果共享數據為空,消費者進入等待狀態,否則將消費共享數據,然後提調用notify方法喚醒生產者線程進行生產。

3. 運行生產者與消費者

下面是生產者與消費者程序的主程序。
public class ThreadCommunication {  
    public static void main(String[] args) {  
        Queue q = new Queue();  
        Producer p = new Producer(q);  
        Consumer c = new Consumer(q);  
        c.start();  
        p.start();  
    }  
}  

注意:考慮到程序的安全性,多數情況下使用 notifiAll(),除非明確可以知道喚醒哪一個線程。wait方法調用的前提條件是當前線程獲取了這個對象的鎖,也就是說 wait方法必須放在同步塊或同步方法中。

6. 線程死鎖

為了保證數據安全使用 synchronized同步機制, 當線程進入堵塞狀態 (不可運行狀態和等待狀態)時,其他線程無法訪問那個加鎖對象(除非同步鎖被解除),所以

一個線程會一直處於等待另一個對象的狀態, 而另一個對象又會處於等待下一個對象的狀態,以此類推,這個線程“等待”狀態鏈會發生很糟糕的情形,即封閉環狀態(也就是說最後那個對象在等待第一個對象的鎖)。此時,所有的線程都陷入毫無止境的等待狀態中,無法繼續運行,這種情況就稱為“死鎖”。雖然這種情況發生的概率很小,一旦出現,程序的調試變得困難而且查錯也是一件很麻煩的事情。

下面舉一個死鎖的例子。

public class ThreadLocked implements Runnable {  
    public static boolean flag = true; // 起一個標志作用  
    private static Object A = new Object(); // 聲明,並初始化靜態Object數據域A  
  
    private static Object B = new Object(); // 聲明,並初始化靜態Object數據域B  
  
    public static void main(String[] args) throws InterruptedException {  
        Runnable r1 = new ThreadLocked(); // 創建,並初始化ThreadLocked對象r1  
        Thread t1 = new Thread(r1); // 創建線程t1  
        Runnable r2 = new ThreadLocked(); // 創建,並初始化ThreadLocked對象r2  
        Thread t2 = new Thread(r2); // 創建線程t2  
        t1.start(); // 啟動線程t1  
        t2.start(); // 啟動線程t2  
    }  
  
    public void AccessA() {  
        flag = false; // 初始化域flag  
        // 同步代碼快  
        synchronized (A) { // 聲明同步塊,給對象A加鎖  
            System.out.println("線程t1 : 我得到了A的鎖"); // 輸出字符串信息  
            try {  
                // 讓當前線程睡眠,從而讓另外一個線程可以先得到對象B的鎖  
                Thread.sleep(1000); // 休眠  
            } catch (InterruptedException e) { // 捕獲異常  
                e.printStackTrace(); // 異常信息輸出  
            }  
            System.out.println("線程t1 : 我還想要得到B的鎖");  
            // 在得到A鎖之後,又想得到B的鎖  
            // 同步塊內部嵌套同步塊  
            synchronized (B) { // 聲明內部嵌套同步塊,指定對象B的鎖  
                System.out.println("線程t1 : 我得到了B的鎖"); // 輸出字符串信息  
            }  
        }  
    }  
  
    public void AccessB() {  
        flag = true; // 修改flag的值  
        // 同步代碼塊  
        synchronized (B) { // 指定同步塊,給B加鎖  
            System.out.println("線程t2 : 我得到了B的鎖"); // 輸出字符串信息  
            try {  
                // 讓當前線程睡眠,從而讓另外一個線程可以先得到對象A的鎖  
                Thread.sleep(1000); // 休眠  
            } catch (InterruptedException e) { // 捕獲異常InterruptedException  
                e.printStackTrace(); // 異常信息輸出  
            }  
            System.out.println("線程t2 : 我還想要得到A的鎖"); // 字符串信息輸出  
            // 在得到B鎖之後,又想得到A的鎖  
            // 同步塊內部嵌套內部快  
            synchronized (A) { // 指定同步塊,給A加鎖  
                System.out.println("線程t2 : 我得到了A的鎖"); // 輸出字符串信息  
            }  
        }  
    }  
  
    public void run() {  
        if (flag){ // 當flag為true,執行下面語句  
            AccessA(); // 調用AccessA方法  
        } else {  
            AccessB(); // 調用AccessB方法  
        }  
    }  
  
}  
程序 ThreadLocked.java中創建了兩個線程 t1 和 t2,並且聲明兩個方法:AccessA和 AccessB。在運行過程中,線程t1 先獲得了 A 的鎖,然後又要求獲得 B 的鎖;而 t2
先獲得B 的鎖,然後又要求獲得 A的鎖,此時便進入了無休止的相互等待狀態,即死鎖。

Java 語言本身並沒有提供防止死鎖的具體方法,但是在具體程序設計時必須要謹慎,以防止出現死鎖現象。通常在程序設計中應注意,不要使用 stop()、suspend()、resume()以及 destroy()方法。 stop()方法不安全,它會解除由該線程獲得的所有對象鎖,而且可能使對象處於不連貫狀態,如果其他線程此時訪問對象,而導致的錯誤很難檢查出來。 suspend()/resume ()方法也極不安全,調用 suspend()方法時,線程會停下來,但是該線程並沒有放棄對象的鎖,導致其他線程並不能獲得對象鎖。調用destroy()會強制終止線程,但是該 線程也不會釋放對象鎖。

Copyright © Linux教程網 All Rights Reserved