java多線程系列----------- 基本的線程機制(一)

能有此文十分感謝《Java編程思想》一書及其作者Bruce Eckel。

一、定義任務

      線程可以驅動任務,因此需要一種描述任務的方式,這可以由Runnable接口來提供。要想定義任務,只需實現Runnable接口並編寫run()方法,使得該任務可以執行你的命令。下面的LiftOff任務將顯示發射之前的倒計時:
public class LiftOff implements Runnable {
	private static int taskCount = 0;
	private final int id = taskCount++;
	
	protected int countDown = 10;//default

	public LiftOff(){
	}
	public LiftOff(int countDown){
		this.countDown = countDown;
	}
	
	public String status() {
		return "#"+id+"("+(countDown > 0 ? countDown : "LiftOff!")+").";
	}
	
	@Override
	public void run() {
		while (countDown-- > 0) {
			System.out.println(status());
			Thread.yield();
		}
	}
}
       標識符id用來區分任務的多個實例,它是final,一旦被初始化就不能修改。
       任務的run()方法通常總會有某種形式的循環,使得任務一直運行下去直到不再需要,所以要設定跳出循環的條件。通常,run()被寫成無限循環的形式,這就意味着除非有某個條件使得run()終止,否則它將永遠運行下去(後面將會看到如何安全終止線程)。
       在run()中對靜態方法Thread.yield()的調用是對線程調度器(java線程機制的一部分,可以將cpu從一個線程轉移給另外一個線程)的一種建議,它在聲明:“我已經執行完生命週期中最重要的部分了,此刻正是切換給其他任務執行一段時間的大好時機。”
       當從Runnable導出一個類時,它必須具有run()方法,但是這個方法並無特殊之處——它不會產生任何內在的線程能力。要實現線程行爲,必須顯式地將一個任務附着到線程上。

二、Thread類

       將Runnable對象轉變爲工作任務的傳統方式是把它提交給一個Thread構造器。下面示例瞭如何使用Thread來驅動LiftOff對象:
public class BasicThreads {
  public static void main(String[] args) {
    Thread t = new Thread(new LiftOff());
    t.start();
    System.out.println("Waiting for LiftOff");
  }
} 
/* Output:
Waiting for LiftOff
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!),
*/
     Thread構造器只需一個Runnable對象。調用Thread對象的start()方法爲該線程的執行必需的初始化操作,然後調用Runnable對象的run()方法,以便在這個新線程中啓動該任務。儘管start()看起來是產生了一個對長期運行方法的調用,但是從輸出中可以看到,start()迅速地返回了,因爲Waiting for LiftOff在倒計時完成之前就出現了。實際上,產生的是對LiftOff.run()的方法的調用,並且這個方法還沒有完成,但是因爲LiftOff.run()是由不同的線程執行的,因此仍舊可以執行main()線程裏其他操作(這種能力並不侷限於main()線程,任何線程都可以啓動另一個線程)。因此,程序會同時運行兩個方法,main()和LiftOff.run()是程序中與其他線程“同時”執行的代碼。
    可以很容易的添加更多的線程去驅動更多的任務,如下:
public class MoreBasicThreads {
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++)
      new Thread(new LiftOff()).start();
    System.out.println("Waiting for LiftOff");
  }
} 
/* Output: 
Waiting for LiftOff
#0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1), #4(1), #0(Liftoff!), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),
*/
輸出說明不同任務的執行在線程被換進換出時混在一起,這種交換是由線程調度器自動控制。在本例中,單一線程(main())在創建所有的LiftOff線程,但是,如果多個線程在創建LiftOff線程,那麼就有可能會有多個LiftOff擁有相同的id,稍後會了解這是爲什麼。
這個程序一次運行的結果可能與另一次不同,這是因爲線程調度機制是非確定性的。

三、使用Executor

       javaSE5中的java.util.concurrent包中的執行器(Executor)可以管理Thread對象,從而簡化併發編程。Executor在客戶端和任務執行之間提供一個間接層;與客戶端直接執行任務不同,這個中介對象將執行任務。Executor管理異步任務的執行,而無需顯式地管理線程的生命週期。Executor在Java SE5/6中是啓動任務的優選方法。
      可以使用Executor來代替MoreBasicThreads .java中顯式地創建Thread對象。LiftOff對象知道如何運行具體的任務,與命令設計模式一樣,它暴露了要執行的單一方法。ExecutorService(具有生命週期的Executor,例如關閉)知道如何構建恰當的上下文來執行Runnable對象。在下面的示例中,CachedThreadPool將爲每個任務都創建一個線程。
import java.util.concurrent.*;

public class CachedThreadPool {
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0; i < 5; i++)
      exec.execute(new LiftOff());
    exec.shutdown();
  }
} 
/* Output: (Sample)
#0(9), #0(8), #1(9), #2(9), #3(9), #4(9), #0(7), #1(8), #2(8), #3(8), #4(8), #0(6), #1(7), #2(7), #3(7), #4(7), #0(5), #1(6), #2(6), #3(6), #4(6), #0(4), #1(5), #2(5), #3(5), #4(5), #0(3), #1(4), #2(4), #3(4), #4(4), #0(2), #1(3), #2(3), #3(3), #4(3), #0(1), #1(2), #2(2), #3(2), #4(2), #0(Liftoff!), #1(1), #2(1), #3(1), #4(1), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),
*/
可以將上面示例中的CachedThreadPool替換爲不同類型的Executor。
FixedThreadPool使用有限的線程集來執行所提交的任務:
import java.util.concurrent.*;

public class FixedThreadPool {
  public static void main(String[] args) {
    // Constructor argument is number of threads:
    ExecutorService exec = Executors.newFixedThreadPool(5);
    for(int i = 0; i < 5; i++)
      exec.execute(new LiftOff());
    exec.shutdown();
  }
} /* Output: (Sample)
#0(9), #0(8), #1(9), #2(9), #3(9), #4(9), #0(7), #1(8), #2(8), #3(8), #4(8), #0(6), #1(7), #2(7), #3(7), #4(7), #0(5), #1(6), #2(6), #3(6), #4(6), #0(4), #1(5), #2(5), #3(5), #4(5), #0(3), #1(4), #2(4), #3(4), #4(4), #0(2), #1(3), #2(3), #3(3), #4(3), #0(1), #1(2), #2(2), #3(2), #4(2), #0(Liftoff!), #1(1), #2(1), #3(1), #4(1), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),
*///:~
FixedThreadPool可以一次性預先執行代價高昂的線程分配,在事件驅動的系統中,需要線程的事件處理器,可以直接從池中獲取線程。
CachedThreadPool在程序執行過程中通常會創建與所需數量相同的線程。
SingleThreadExecutor就像是數量爲1的FixedThreadPool,它還提供了一種重要的併發保證既沒有兩個線程會被併發調用。如果向SingleThreadExecutor提交了多個任務,那麼這些任務將排隊,SingleThreadExecutor會序列化所有提交給它的任務,並會維護它自己的懸掛任務隊列,每個任務都會在下一個任務開始之前運行結束,所有的任務將使用相同的線程,如下面示例:
import java.util.concurrent.*;

public class SingleThreadExecutor {
  public static void main(String[] args) {
    ExecutorService exec =
      Executors.newSingleThreadExecutor();
    for(int i = 0; i < 5; i++)
      exec.execute(new LiftOff());
    exec.shutdown();
  }
} /* Output:
#0(9), #0(8), #0(7), #0(6), #0(5), #0(4), #0(3), #0(2), #0(1), #0(Liftoff!), #1(9), #1(8), #1(7), #1(6), #1(5), #1(4), #1(3), #1(2), #1(1), #1(Liftoff!), #2(9), #2(8), #2(7), #2(6), #2(5), #2(4), #2(3), #2(2), #2(1), #2(Liftoff!), #3(9), #3(8), #3(7), #3(6), #3(5), #3(4), #3(3), #3(2), #3(1), #3(Liftoff!), #4(9), #4(8), #4(7), #4(6), #4(5), #4(4), #4(3), #4(2), #4(1), #4(Liftoff!),
*/
    假設有大量的線程,並且它們運行的任務將使用文件系統,就可以用SingleThreadExecutor 來運行這些線程,以確保任意時刻在任何線程中都只有唯一的任務在運行。在這種情況下,就不需要在共享資源上處理同步(同時不會過度使用文件系統)。

四、從任務中產生返回值

       Runnable是執行工作的獨立任務,但是它不返回任何值。如果希望在任務完成時能夠返回一個值,那麼可以實現Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一種具有類型參數的泛型,它的類型參數表示的是從方法call()中返回的值,並且必須使用ExecutorService.submit()方法調用它,下面是一個簡單示例:
import java.util.concurrent.*;
import java.util.*;

class TaskWithResult implements Callable<String> {
  private int id;
  public TaskWithResult(int id) {
    this.id = id;
  }
  public String call() {
    return "result of TaskWithResult " + id;
  }
}

public class CallableDemo {
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    ArrayList<Future<String>> results =
      new ArrayList<Future<String>>();
    for(int i = 0; i < 10; i++)
      results.add(exec.submit(new TaskWithResult(i)));
    for(Future<String> fs : results)
      try {
        // get() blocks until completion:
        System.out.println(fs.get());
      } catch(InterruptedException e) {
        System.out.println(e);
        return;
      } catch(ExecutionException e) {
        System.out.println(e);
      } finally {
        exec.shutdown();
      }
  }
} /* Output:
result of TaskWithResult 0
result of TaskWithResult 1
result of TaskWithResult 2
result of TaskWithResult 3
result of TaskWithResult 4
result of TaskWithResult 5
result of TaskWithResult 6
result of TaskWithResult 7
result of TaskWithResult 8
result of TaskWithResult 9
*/
   submit()方法會產生Future對象,它用Callable返回結果的特定類型進行了參數化。可以用isDone()方法來查詢Future是否已經完成。當任務完成時,它具有一個結果,可以調用get()方法來獲取結果。也可以不用isDone()進行檢查就直接調用get(),這種情況下get()將阻塞,直至結果準備就緒。還可以在試圖調用get()獲取結果之前,先調用具有超時的get()或者調用isDone()來查看任務是否完成。

五、休眠

   影響任務行爲的一種簡單方法是調用sleep(),這將使任務中止執行給定的時間。在LiftOff類中,把對yield()的調用換成調用sleep(),將可能得到如下結果:
import java.util.concurrent.*;


public class SleepingTask extends LiftOff {
  public void run() {
    try {
      while(countDown-- > 0) {
        System.out.print(status());
        // Old-style:
        // Thread.sleep(100);
        // Java SE5/6-style:
        TimeUnit.MILLISECONDS.sleep(100);
      }
    } catch(InterruptedException e) {
      System.err.println("Interrupted");
    }
  }
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0; i < 5; i++)
      exec.execute(new SleepingTask());
    exec.shutdown();
  }
} /* Output:
#0(9), #1(9), #2(9), #3(9), #4(9), #0(8), #1(8), #2(8), #3(8), #4(8), #0(7), #1(7), #2(7), #3(7), #4(7), #0(6), #1(6), #2(6), #3(6), #4(6), #0(5), #1(5), #2(5), #3(5), #4(5), #0(4), #1(4), #2(4), #3(4), #4(4), #0(3), #1(3), #2(3), #3(3), #4(3), #0(2), #1(2), #2(2), #3(2), #4(2), #0(1), #1(1), #2(1), #3(1), #4(1), #0(Liftoff!), #1(Liftoff!), #2(Liftoff!), #3(Liftoff!), #4(Liftoff!),
*/
   對sleep()的調用可以拋出InterruptedException異常,它在run()中被捕獲。因爲異常不能跨線程傳播回main(),所以必須在本地處理所以任務內部產生的異常。
   Java SE5引入了更加顯式地sleep()版本,作爲TimeUnit類的一部分,就像上面示例所示,這個方法允許指定延遲的時間單元。TimeUnit還可以被用來執行轉換。
   可能會注意到,這些任務是按照“完美的分佈”順序執行的,即從0到4,然後再回過頭從0開始,當然這取決於你的平臺,依賴於底層的線程機制,這種機制在不同的操作系統之間是有差異的,因此不能依賴於它。如果必須控制任務執行的順序,最好使用同步控制或壓根不使用線程,但是要編寫自己的協作例程,這些例程將會按照指定的順序在互相之間傳遞控制權。

六、優先級

      線程的優先級將該線程的重要性傳遞給調度器,調度器將傾向於讓優先權最高的線程先執行,然而,這並不意味着優先權較低的線程得不到執行,優先級較低的線程僅僅是執行的頻率較低。
     在絕大多數時間裏,所有線程都應該以默認的優先級執行。試圖操縱線程優先級通常是一種錯誤。
     下面是一個演示優先級的示例。可以用getPriority()來讀取現有線程的優先級,並且在任何時候都可以通過setPriority()來修改它。
import java.util.concurrent.*;

public class SimplePriorities implements Runnable {
  private int countDown = 5;
  private volatile double d; // No optimization
  private int priority;
  public SimplePriorities(int priority) {
    this.priority = priority;
  }
  public String toString() {
    return Thread.currentThread() + ": " + countDown;
  }
  public void run() {
    Thread.currentThread().setPriority(priority);
    while(true) {
      // An expensive, interruptable operation:
      for(int i = 1; i < 100000; i++) {
        d += (Math.PI + Math.E) / (double)i;
        if(i % 1000 == 0)
          Thread.yield();
      }
      System.out.println(this);
      if(--countDown == 0) return;
    }
  }
  public static void main(String[] args) {
    ExecutorService exec = Executors.newCachedThreadPool();
    for(int i = 0; i < 5; i++)
      exec.execute(
        new SimplePriorities(Thread.MIN_PRIORITY));
    exec.execute(
        new SimplePriorities(Thread.MAX_PRIORITY));
    exec.shutdown();
  }
} /* Output: (70% match)
Thread[pool-1-thread-6,10,main]: 5
Thread[pool-1-thread-6,10,main]: 4
Thread[pool-1-thread-6,10,main]: 3
Thread[pool-1-thread-6,10,main]: 2
Thread[pool-1-thread-6,10,main]: 1
Thread[pool-1-thread-3,1,main]: 5
Thread[pool-1-thread-2,1,main]: 5
Thread[pool-1-thread-1,1,main]: 5
Thread[pool-1-thread-5,1,main]: 5
Thread[pool-1-thread-4,1,main]: 5
...
*/
  toString()方法被覆蓋,以便使用Thread.toString()方法打印線程的名稱、線程的優先級以及線程所屬的“線程組”。可以在一個任務的內部,通過調用Thread.currentThread()來獲得對驅動該任務的Thread對象的引用。
   注意優先級實在run()的開頭部分設定的,在構造器中設置不會有任何好處,因爲Executor在此時還沒有開始執行任務。
   儘管JDK有10個優先級,但是它與多數操作系統都不能映射得很好,唯一可移植的方法是當調整優先級的時候,只使用MAX_PRIORITY、NORM_PRIORITY和MIN_PRIORITY三種級別。



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