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毫秒,說明其高效性。大家感興趣的可自行檢視、瞭解該塊的知識點。
好了,以上概括的就是多執行緒的基本知識點了,希望幫到大家。