1. 程式人生 > >Java語言學習(十二):多執行緒

Java語言學習(十二):多執行緒

    Java中給多執行緒程式設計提供了內建的支援,多執行緒是多工的一種特別形式,它使用了更小的資源開銷。這裡需要知道兩個術語及其關係:程序和執行緒。

    程序:程序是系統進行資源分配和排程的一個獨立單位。一個程序包括由作業系統分配的記憶體空間,包含一個或多個執行緒。

    執行緒:執行緒是程序的一個實體,是CPU排程和分派的基本單位。它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。

    (一)執行緒的生命週期

    執行緒是一個動態執行的過程,從產生到死亡,這個過程稱為執行緒的生命週期。執行緒的狀態有:新建狀態、就緒狀態、執行狀態、阻塞狀態、死亡狀態,如下圖所示,注意整個執行過程的實現:

  • 新建狀態(New):當執行緒物件對建立後,即進入了新建狀態;
  • 就緒狀態(Runnable):當呼叫執行緒物件的start()方法,執行緒即進入就緒狀態,等待CPU排程執行;
  • 執行狀態(Running):當CPU開始排程處於就緒狀態的執行緒時,此時執行緒才得以真正執行,即進入到執行狀態;
  • 阻塞狀態(Blocked):處於執行狀態中的執行緒由於某種原因,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU呼叫以進入到執行狀態;
  • 死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期;

    (二):執行緒的建立

    Java提供了三種建立執行緒的方法:

  • 實現Runnable介面
  • 繼承Thread類
  • 通過Callable和Future建立; 

    在開發中,前兩種是常用的執行緒建立方式,下面來簡單說下:

     (1)通過實現Runnable介面建立執行緒

public class RunnableDemo implements Runnable{
    private Thread t;
    private String threadName;

    //構造方法
    public RunnableDemo(String name) {
        this.threadName = name;
        System.out.println("建立執行緒:"+threadName);
    }
    //重寫run()方法
    @Override
	public void run() {
        System.out.println("執行執行緒:"+threadName);
        try {
            for(int i=4;i>0;i--){
                System.out.println("Thread: "+threadName+","+i);
                Thread.sleep(50);
            }
        }catch (Exception e) {
             System.out.println("Thread "+threadName+" 阻塞");
        }
        System.out.println("Thread "+threadName+" 終止"); 
    }
    //呼叫方法(為了輸出資訊,可以忽略)
    public void start(){
        System.out.println("啟動執行緒:"+threadName);
        if(t == null){
           t = new Thread(this,threadName);
           t.start(); 
        }  
    }
}

    測試類:

public class RunnableTest {
    public static void main(String[] args) {
        RunnableDemo r1 = new RunnableDemo("T1");
        r1.start();
        RunnableDemo r2 = new RunnableDemo("T2");
        r2.start();
    }   
}

    輸出為:

建立執行緒:T1
啟動執行緒:T1
建立執行緒:T2
啟動執行緒:T2
執行執行緒:T1
Thread: T1
執行執行緒:T2
Thread: T2,4
Thread: T2,3
Thread: T1,3
Thread: T2,2
Thread: T1,2
Thread: T2,1
Thread: T1,1
Thread T2 終止
Thread T1 終止

    從上面的例項可以看出,通過實現Runnable介面建立執行緒的幾個要點:

  • 構造方法來建立執行緒物件,有參或無參看自己需要;
  • 重寫run()方法,這裡寫入自己需要實現的程式碼;
  • 啟動start()方法,這裡可以直接呼叫;
  • run()方法是執行緒的入口點,必須通過呼叫start()方法才能執行;

    (2)通過繼承Thread類建立執行緒

    它本質上也是實現了 Runnable 介面的一個例項,所以這裡就不貼出程式碼了,可以按照上面的例項,更改class為繼承即可,如下:

public class ThreadDemo extends Thread{}

    Thread類的常用且重要的方法有:

  • public void start():使該執行緒開始執行;Java虛擬機器呼叫該執行緒的 run 方法,物件呼叫;
  • public void run():物件呼叫;
  • public static void sleep(long millisec):在指定的毫秒數內讓當前正在執行的執行緒休眠,靜態方法,直接呼叫;

     注意:Java虛擬機器允許應用程式併發的執行多個執行執行緒,利用多執行緒程式設計可以編寫高效的程式,但執行緒太多,CPU 花費在上下文的切換的時間將多於執行程式的時間,執行效率反而降低,所以,執行緒並不是建立的越多越好好,一般來說小到1個,大到10左右基本就夠用了。

    當然,關於執行緒的其他知識,如優先順序、休眠、終止等,這裡就不做介紹了。

    (三)synchronized關鍵字

    Java提供了很多方式和工具來幫助簡化多執行緒的開發,如同步方法,即有synchronized關鍵字修飾的方法,這和Java的內建鎖有關。每個Java物件都有一個內建鎖,若方法用synchronized關鍵字宣告,則內建鎖會保護整個方法,即在呼叫該方法前,需要獲得內建鎖,否則就處於阻塞狀態。一個簡單的同步方法宣告如下:

public synchronized void save(){}

    synchronized關鍵字也可以修飾靜態方法,此時若呼叫該靜態方法,則會鎖住整個類。下面通過例項來說明下具體的使用:

    同步執行緒類:

public class SyncThread implements Runnable {
   //定義計數變數並在建構函式中初始化
   private static int count;
   public SyncThread(){
	   count = 0;
   }
   @Override
   public synchronized void run() {
       for(int i=0;i<5;i++){
          //列印當前count值並進行累加操作,可分開寫
          System.out.println(Thread.currentThread().getName() +":"+ (count++));
          try {
             Thread.sleep(100);
          }catch (InterruptedException e) {
             e.printStackTrace();
          }
       }
   }
   public int getCount(){
		return count;
   }  
}

    測試類:

public class SyncTest {
    public static void main(String[] args) {
         SyncThread sThread = new SyncThread();
         //建立執行緒物件的同時初始化該執行緒的名稱
         Thread t1 = new Thread(sThread,"SThread1");
         Thread t2 = new Thread(sThread,"SThread2");
         t1.start();
		 t2.start();
    }
}

    輸出為:

SThread1:0
SThread1:1
SThread1:2
SThread1:3
SThread1:4
SThread2:5
SThread2:6
SThread2:7
SThread2:8
SThread2:9

    從上面可以看出:一個執行緒訪問一個物件中的synchronized同步方法時,其他試圖訪問該物件的執行緒將被阻塞。當然,大家可以去掉synchronized關鍵字,看看會有什麼不同。這裡必須要注意:是訪問同一個物件的不同方法,如上面的物件sThread,若是不同的物件,則不受阻塞。這裡不做介紹了,大家可以參考:Java中synchronized的用法,好好理解下。

    (四)volatile關鍵字

    相比較synchronized而言,volatile關鍵字是Java提供的一種輕量級的同步機制,為域變數的訪問提供了一種免鎖機制,使用volatile修飾域相當於告訴虛擬機器該域可能會被其他執行緒更新,因此每次使用該域就要重新計算,而不是使用暫存器中的值。

    如果讀操作的次數要遠遠超過寫操作,與鎖相比,volatile 變數通常能夠減少同步的效能開銷。簡單的定義如下:

private volatile int count = 0;

    volatile不具備原子特性,也不能用來修飾final型別的變數。要使volatile修飾的變數提供理想的執行緒安全,必須滿足兩個條件:

  • 對變數的寫操作不依賴於當前值;
  • 變數沒有包含在具有其他變數的不變式中;

    這裡不做詳述了,但需要注意一點:避免volatile修飾的變數用於複合操作,如 num++,這個複合操作包括三步(讀取->加1->賦值),所以,在多執行緒的環境下,有可能執行緒會對過期的num進行++操作,重新寫入到主存中,而導致出現num的結果不合預期的情況。

    執行緒間還可以實現通訊,這裡不做介紹。

    Java中的物件使用new操作符建立,若建立大量短生命週期的物件,則效能低下。所以才有了池的技術,如資料庫連線有連線池,執行緒則有執行緒池。

    使用執行緒池建立物件的時間是0毫秒,說明其高效性。大家感興趣的可自行檢視、瞭解該塊的知識點。

    好了,以上概括的就是多執行緒的基本知識點了,希望幫到大家。