Java 多線程編程

2022-01-25 15:45 更新

Java 給多線程編程提供了內(nèi)置的支持。一個(gè)多線程程序包含兩個(gè)或多個(gè)能并發(fā)運(yùn)行的部分。程序的每一部分都稱作一個(gè)線程,并且每個(gè)線程定義了一個(gè)獨(dú)立的執(zhí)行路徑。

多線程是多任務(wù)的一種特別的形式。多線程比多任務(wù)需要更小的開銷。

這里定義和線程相關(guān)的另一個(gè)術(shù)語(yǔ):進(jìn)程:一個(gè)進(jìn)程包括由操作系統(tǒng)分配的內(nèi)存空間,包含一個(gè)或多個(gè)線程。一個(gè)線程不能獨(dú)立的存在,它必須是進(jìn)程的一部分。一個(gè)進(jìn)程一直運(yùn)行,直到所有的非守候線程都結(jié)束運(yùn)行后才能結(jié)束。

多線程能滿足程序員編寫非常有效率的程序來(lái)達(dá)到充分利用 CPU 的目的,因?yàn)?CPU 的空閑時(shí)間能夠保持在最低限度。


一個(gè)線程的生命周期

線程經(jīng)過其生命周期的各個(gè)階段。下圖顯示了一個(gè)線程完整的生命周期。

線程 

  • 新建(new Thread)

    當(dāng)創(chuàng)建Thread類的一個(gè)實(shí)例(對(duì)象)時(shí),此線程進(jìn)入新建狀態(tài)(未被啟動(dòng))。
    例如:Thread  t1=new Thread();
  • 就緒(runnable)
    線程已經(jīng)被啟動(dòng),正在等待被分配給 CPU 時(shí)間片,也就是說(shuō)此時(shí)線程正在就緒隊(duì)列中排隊(duì)等候得到 CPU 資源。
    例如:t1.start();

  • 運(yùn)行(running)
    線程獲得 CPU 資源正在執(zhí)行任務(wù)( run() 方法),此時(shí)除非此線程自動(dòng)放棄 CPU 資源或者有優(yōu)先級(jí)更高的線程進(jìn)入,線程將一直運(yùn)行到結(jié)束。

  • 堵塞(blocked)由于某種原因?qū)е抡谶\(yùn)行的線程讓出CPU并暫停自己的執(zhí)行,即進(jìn)入堵塞狀態(tài)。

    正在睡眠:用 sleep(long t) 方法可使線程進(jìn)入睡眠方式。一個(gè)睡眠著的線程在指定的時(shí)間過去可進(jìn)入就緒狀態(tài)。

    正在等待:調(diào)用 wait() 方法。(調(diào)用 motify() 方法回到就緒狀態(tài))

    被另一個(gè)線程所阻塞:調(diào)用 suspend() 方法。(調(diào)用 resume() 方法恢復(fù))

  • 死亡(dead)當(dāng)線程執(zhí)行完畢或被其它線程殺死,線程就進(jìn)入死亡狀態(tài),這時(shí)線程不可能再進(jìn)入就緒狀態(tài)等待執(zhí)行。

    自然終止:正常運(yùn)行 run() 方法后終止

    異常終止:調(diào)用 stop() 方法讓一個(gè)線程終止運(yùn)行


線程的優(yōu)先級(jí)

每一個(gè) Java 線程都有一個(gè)優(yōu)先級(jí),這樣有助于操作系統(tǒng)確定線程的調(diào)度順序。Java 優(yōu)先級(jí)在 MIN_PRIORITY(1)和 MAX_PRIORITY(10)之間的范圍內(nèi)。默認(rèn)情況下,每一個(gè)線程都會(huì)分配一個(gè)優(yōu)先級(jí)NORM_PRIORITY(5)。

具有較高優(yōu)先級(jí)的線程對(duì)程序更重要,并且應(yīng)該在低優(yōu)先級(jí)的線程之前分配處理器時(shí)間。然而,線程優(yōu)先級(jí)不能保證線程執(zhí)行的順序,而且非常依賴于平臺(tái)。


創(chuàng)建一個(gè)線程

Java 提供了三種創(chuàng)建線程方法:

  • 通過實(shí)現(xiàn) Runnable 接口;

  • 通過繼承 Thread 類本身;

  • 通過 Callable 和 Future 創(chuàng)建線程。


通過實(shí)現(xiàn) Runnable 接口來(lái)創(chuàng)建線程

創(chuàng)建一個(gè)線程,最簡(jiǎn)單的方法是創(chuàng)建一個(gè)實(shí)現(xiàn) Runnable 接口的類。

為了實(shí)現(xiàn) Runnable,一個(gè)類只需要執(zhí)行一個(gè)方法調(diào)用 run(),聲明如下:

public void run()

你可以重寫該方法,重要的是理解的 run() 可以調(diào)用其他方法,使用其他類,并聲明變量,就像主線程一樣。

在創(chuàng)建一個(gè)實(shí)現(xiàn) Runnable 接口的類之后,你可以在類中實(shí)例化一個(gè)線程對(duì)象。

Thread定義了幾個(gè)構(gòu)造方法,下面的這個(gè)是我們經(jīng)常使用的:

Thread(Runnable threadOb,String threadName);

這里,threadOb 是一個(gè)實(shí)現(xiàn) Runnable 接口的類的實(shí)例,并且 threadName 指定新線程的名字。

新線程創(chuàng)建之后,你調(diào)用它的start()方法它才會(huì)運(yùn)行。

void start();

實(shí)例

下面是一個(gè)創(chuàng)建線程并開始讓它執(zhí)行的實(shí)例:

// 創(chuàng)建一個(gè)新的線程
class NewThread implements Runnable {
   Thread t;
   NewThread() {
      // 創(chuàng)建第二個(gè)新線程
      t = new Thread(this, "Demo Thread");
      System.out.println("Child thread: " + t);
      t.start(); // 開始線程
   }
  
   // 第二個(gè)線程入口
   public void run() {
      try {
         for(int i = 5; i > 0; i--) {
            System.out.println("Child Thread: " + i);
            // 暫停線程
            Thread.sleep(50);
         }
     } catch (InterruptedException e) {
         System.out.println("Child interrupted.");
     }
     System.out.println("Exiting child thread.");
   }
}
 
public class ThreadDemo {
   public static void main(String args[]) {
      new NewThread(); // 創(chuàng)建一個(gè)新線程
      try {
         for(int i = 5; i > 0; i--) {
           System.out.println("Main Thread: " + i);
           Thread.sleep(100);
         }
      } catch (InterruptedException e) {
         System.out.println("Main thread interrupted.");
      }
      System.out.println("Main thread exiting.");
   }
}

編譯以上程序運(yùn)行結(jié)果如下:

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.

通過繼承 Thread 來(lái)創(chuàng)建線程

創(chuàng)建一個(gè)線程的第二種方法是創(chuàng)建一個(gè)新的類,該類繼承 Thread 類,然后創(chuàng)建一個(gè)該類的實(shí)例。

繼承類必須重寫 run() 方法,該方法是新線程的入口點(diǎn)。它也必須調(diào)用 start() 方法才能執(zhí)行。

該方法盡管被列為一種多線程實(shí)現(xiàn)方式,但是本質(zhì)上也是實(shí)現(xiàn)了 Runnable 接口的一個(gè)實(shí)例。

實(shí)例

// 通過繼承 Thread 創(chuàng)建線程
class NewThread extends Thread {
   NewThread() {
      // 創(chuàng)建第二個(gè)新線程
      super("Demo Thread");
      System.out.println("Child thread: " + this);
      start(); // 開始線程
   }
 
   // 第二個(gè)線程入口
   public void run() {
      try {
         for(int i = 5; i > 0; i--) {
            System.out.println("Child Thread: " + i);
                            // 讓線程休眠一會(huì)
            Thread.sleep(50);
         }
      } catch (InterruptedException e) {
         System.out.println("Child interrupted.");
      }
      System.out.println("Exiting child thread.");
   }
}
 
public class ExtendThread {
   public static void main(String args[]) {
      new NewThread(); // 創(chuàng)建一個(gè)新線程
      try {
         for(int i = 5; i > 0; i--) {
            System.out.println("Main Thread: " + i);
            Thread.sleep(100);
         }
      } catch (InterruptedException e) {
         System.out.println("Main thread interrupted.");
      }
      System.out.println("Main thread exiting.");
   }
}

編譯以上程序運(yùn)行結(jié)果如下:

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.

Thread 方法

下表列出了Thread類的一些重要方法:

序號(hào) 方法描述
1 public void start()
使該線程開始執(zhí)行;Java 虛擬機(jī)調(diào)用該線程的 run 方法。
2 public void run()
如果該線程是使用獨(dú)立的 Runnable 運(yùn)行對(duì)象構(gòu)造的,則調(diào)用該 Runnable 對(duì)象的 run 方法;否則,該方法不執(zhí)行任何操作并返回。
3 public final void setName(String name)
改變線程名稱,使之與參數(shù) name 相同。
4 public final void setPriority(int priority)
 更改線程的優(yōu)先級(jí)。
5 public final void setDaemon(boolean on)
將該線程標(biāo)記為守護(hù)線程或用戶線程。
6 public final void join(long millisec)
等待該線程終止的時(shí)間最長(zhǎng)為 millis 毫秒。
7 public void interrupt()
中斷線程。
8 public final boolean isAlive()
測(cè)試線程是否處于活動(dòng)狀態(tài)。

測(cè)試線程是否處于活動(dòng)狀態(tài)。 上述方法是被Thread對(duì)象調(diào)用的。下面的方法是Thread類的靜態(tài)方法。

序號(hào) 方法描述
1 public static void yield()
暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程。
2 public static void sleep(long millisec)
在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠(暫停執(zhí)行),此操作受到系統(tǒng)計(jì)時(shí)器和調(diào)度程序精度和準(zhǔn)確性的影響。
3 public static boolean holdsLock(Object x)
當(dāng)且僅當(dāng)當(dāng)前線程在指定的對(duì)象上保持監(jiān)視器鎖時(shí),才返回 true。
4 public static Thread currentThread()
返回對(duì)當(dāng)前正在執(zhí)行的線程對(duì)象的引用。
5 public static void dumpStack()
將當(dāng)前線程的堆棧跟蹤打印至標(biāo)準(zhǔn)錯(cuò)誤流。

實(shí)例

如下的ThreadClassDemo 程序演示了Thread類的一些方法:

// 文件名 : DisplayMessage.java
// 通過實(shí)現(xiàn) Runnable 接口創(chuàng)建線程
public class DisplayMessage implements Runnable
{
   private String message;
   public DisplayMessage(String message)
   {
      this.message = message;
   }
   public void run()
   {
      while(true)
      {
         System.out.println(message);
      }
   }
}

GuessANumber.java 文件代碼:

// 文件名 : GuessANumber.java
// 通過繼承 Thread 類創(chuàng)建線程

public class GuessANumber extends Thread
{
   private int number;
   public GuessANumber(int number)
   {
      this.number = number;
   }
   public void run()
   {
      int counter = 0;
      int guess = 0;
      do
      {
          guess = (int) (Math.random() * 100 + 1);
          System.out.println(this.getName()
                       + " guesses " + guess);
          counter++;
      }while(guess != number);
      System.out.println("** Correct! " + this.getName()
                       + " in " + counter + " guesses.**");
   }
}

ThreadClassDemo.java 文件代碼:

// 文件名 : ThreadClassDemo.java
public class ThreadClassDemo
{
   public static void main(String [] args)
   {
      Runnable hello = new DisplayMessage("Hello");
      Thread thread1 = new Thread(hello);
      thread1.setDaemon(true);
      thread1.setName("hello");
      System.out.println("Starting hello thread...");
      thread1.start();
     
      Runnable bye = new DisplayMessage("Goodbye");
      Thread thread2 = new Thread(bye);
      thread2.setPriority(Thread.MIN_PRIORITY);
      thread2.setDaemon(true);
      System.out.println("Starting goodbye thread...");
      thread2.start();
 
      System.out.println("Starting thread3...");
      Thread thread3 = new GuessANumber(27);
      thread3.start();
      try
      {
         thread3.join();
      }catch(InterruptedException e)
      {
         System.out.println("Thread interrupted.");
      }
      System.out.println("Starting thread4...");
      Thread thread4 = new GuessANumber(75);
     
           thread4.start();
      System.out.println("main() is ending...");
   }
}

運(yùn)行結(jié)果如下,每一次運(yùn)行的結(jié)果都不一樣。

Starting hello thread...
Starting goodbye thread...
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Starting thread3...
Hello
Hello
Starting thread4...
Hello
Hello
main() is ending...

通過 Callable 和 Future 創(chuàng)建線程

  • 1. 創(chuàng)建 Callable 接口的實(shí)現(xiàn)類,并實(shí)現(xiàn) call() 方法,該 call() 方法將作為線程執(zhí)行體,并且有返回值。
  • 2. 創(chuàng)建 Callable 實(shí)現(xiàn)類的實(shí)例,使用 FutureTask 類來(lái)包裝 Callable 對(duì)象,該 FutureTask 對(duì)象封裝了該 Callable 對(duì)象的 call() 方法的返回值。
  • 3. 使用 FutureTask 對(duì)象作為 Thread 對(duì)象的 target 創(chuàng)建并啟動(dòng)新線程。
  • 4. 調(diào)用 FutureTask 對(duì)象的 get() 方法來(lái)獲得子線程執(zhí)行結(jié)束后的返回值。

實(shí)例

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循環(huán)變量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的線程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子線程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

創(chuàng)建線程的三種方式的對(duì)比

  • 1. 采用實(shí)現(xiàn) Runnable、Callable 接口的方式創(chuàng)建多線程時(shí),線程類只是實(shí)現(xiàn)了 Runnable 接口或 Callable 接口,還可以繼承其他類。
  • 2. 使用繼承 Thread 類的方式創(chuàng)建多線程時(shí),編寫簡(jiǎn)單,如果需要訪問當(dāng)前線程,則無(wú)需使用 Thread.currentThread() 方法,直接使用 this 即可獲得當(dāng)前線程。

線程的幾個(gè)主要概念

在多線程編程時(shí),你需要了解以下幾個(gè)概念:

  • 線程同步

  • 線程間通信

  • 線程死鎖

  • 線程控制:掛起、停止和恢復(fù)


多線程的使用

有效利用多線程的關(guān)鍵是理解程序是并發(fā)執(zhí)行而不是串行執(zhí)行的。例如:程序中有兩個(gè)子系統(tǒng)需要并發(fā)執(zhí)行,這時(shí)候就需要利用多線程編程。

通過對(duì)多線程的使用,可以編寫出非常高效的程序。不過請(qǐng)注意,如果你創(chuàng)建太多的線程,程序執(zhí)行的效率實(shí)際上是降低了,而不是提升了。

請(qǐng)記住,上下文的切換開銷也很重要,如果你創(chuàng)建了太多的線程,CPU 花費(fèi)在上下文的切換的時(shí)間將多于執(zhí)行程序的時(shí)間!


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)