《程式設計思想之多執行緒與多程式(1)——以作業系統的角度述說執行緒與程式》一文詳細講述了執行緒、程式的關係及在作業系統中的表現,這是多執行緒學習必須瞭解的基礎。本文將接著講一下Java中多執行緒程式的開發。
單執行緒
任何程式至少有一個執行緒,即使你沒有主動地建立執行緒,程式從一開始執行就有一個預設的執行緒,被稱為主執行緒,只有一個執行緒的程式稱為單執行緒程式。如下面這一簡單的程式碼,沒有顯示地建立一個執行緒,程式從main開始執行,main本身就是一個執行緒(主執行緒),單個執行緒從頭執行到尾。
【Demo1】:單執行緒程式
1 2 3 4 5 6 |
public static void main(String args[]) { System.out.println("輸出從1到100的數:"); for (int i = 0; i < 100; i ++) { System.out.println(i + 1); } } |
建立執行緒
單執行緒程式簡單明瞭,但有時無法滿足特定的需求。如一個文書處理的程式,我在列印文章的同時也要能對文字進行編輯,如果是單執行緒的程式則要等印表機列印完成之後你才能對文字進行編輯,但列印的過程一般比較漫長,這是我們無法容忍的。如果採用多執行緒,列印的時候可以單獨開一個執行緒去列印,主執行緒可以繼續進行文字編輯。在程式需要同時執行多個任務時,可以採用多執行緒。
在程式需要同時執行多個任務時,可以採用多執行緒。Java給多執行緒程式設計提供了內建的支援,提供了兩種建立執行緒方法:1.通過實現Runable介面;2.通過繼承Thread類。
Thread是JDK實現的對執行緒支援的類,Thread類本身實現了Runnable介面,所以Runnable是顯示建立執行緒必須實現的介面; Runnable只有一個run方法,所以不管通過哪種方式建立執行緒,都必須實現run方法。我們可以看一個例子。
【Demo2】:執行緒的建立和使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
/** * Created with IntelliJ IDEA. * User: luoweifu * Date: 15-5-24 * Time: 下午9:30 * To change this template use File | Settings | File Templates. */ /** * 通過實現Runnable方法 */ class ThreadA implements Runnable { private Thread thread; private String threadName; public ThreadA(String threadName) { thread = new Thread(this, threadName); this.threadName = threadName; } //實現run方法 public void run() { for (int i = 0; i < 100; i ++) { System.out.println(threadName + ": " + i); } } public void start() { thread.start(); } } /** * 繼承Thread的方法 */ class ThreadB extends Thread { private String threadName; public ThreadB(String threadName) { super(threadName); this.threadName = threadName; } //實現run方法 public void run() { for (int i = 0; i < 100; i ++) { System.out.println(threadName + ": " + i); } } } public class MultiThread{ public static void main(String args[]) { ThreadA threadA = new ThreadA("ThreadA"); ThreadB threadB = new ThreadB("ThreadB"); threadA.start(); threadB.start(); } } |
說明:上面的例子中例舉了兩種實現執行緒的方式。大部分情況下選擇實現Runnable介面的方式會優於繼承Thread的方式,因為:
1. 從 Thread 類繼承會強加類層次;
2. 有些類不能繼承Thread類,如要作為執行緒執行的類已經是某一個類的子類了,但Java只支援單繼承,所以不能再繼承Thread類了。
執行緒同步
執行緒與執行緒之間的關係,有幾種:
模型一:簡單的執行緒,多個執行緒同時執行,但各個執行緒處理的任務毫不相干,沒有資料和資源的共享,不會出現爭搶資源的情況。這種情況下不管有多少個執行緒同時執行都是安全的,其執行模型如下:
圖 1:處理相互獨立的任務
模型二:複雜的執行緒,多個執行緒共享相同的資料或資源,就會出現多個執行緒爭搶一個資源的情況。這時就容易造成資料的非預期(錯誤)處理,是執行緒不安全的,其模型如下:
圖 2:多個執行緒共享相同的資料或資源
在出現模型二的情況時就要考慮執行緒的同步,確保執行緒的安全。Java中對執行緒同步的支援,最常見的方式是新增synchronized同步鎖。
我們通過一個例子來看一下執行緒同步的應用。
買火車票是大家春節回家最為關注的事情,我們就簡單模擬一下火車票的售票系統(為使程式簡單,我們就抽出最簡單的模型進行模擬):有500張從北京到贛州的火車票,在8個視窗同時出售,保證系統的穩定性和資料的原子性。
圖 3:模擬火車票售票系統
【Demo3】:火車票售票系統模擬程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
/** * 模擬伺服器的類 */ class Service { private String ticketName; //票名 private int totalCount; //總票數 private int remaining; //剩餘票數 public Service(String ticketName, int totalCount) { this.ticketName = ticketName; this.totalCount = totalCount; this.remaining = totalCount; } public synchronized int saleTicket(int ticketNum) { if (remaining > 0) { remaining -= ticketNum; try { //暫停0.1秒,模擬真實系統中複雜計算所用的時間 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (remaining >= 0) { return remaining; } else { remaining += ticketNum; return -1; } } return -1; } public synchronized int getRemaining() { return remaining; } public String getTicketName() { return this.ticketName; } } /** * 售票程式 */ class TicketSaler implements Runnable { private String name; private Service service; public TicketSaler(String windowName, Service service) { this.name = windowName; this.service = service; } @Override public void run() { while (service.getRemaining() > 0) { synchronized (this) { System.out.print(Thread.currentThread().getName() + "出售第" + service.getRemaining() + "張票,"); int remaining = service.saleTicket(1); if (remaining >= 0) { System.out.println("出票成功!剩餘" + remaining + "張票."); } else { System.out.println("出票失敗!該票已售完。"); } } } } } |
測試程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * 測試類 */ public class TicketingSystem { public static void main(String args[]) { Service service = new Service("北京-->贛州", 500); TicketSaler ticketSaler = new TicketSaler("售票程式", service); //建立8個執行緒,以模擬8個視窗 Thread threads[] = new Thread[8]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(ticketSaler, "視窗" + (i + 1)); System.out.println("視窗" + (i + 1) + "開始出售 " + service.getTicketName() + " 的票..."); threads[i].start(); } } } |
結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
視窗1開始出售 北京–>贛州 的票… 視窗2開始出售 北京–>贛州 的票… 視窗3開始出售 北京–>贛州 的票… 視窗4開始出售 北京–>贛州 的票… 視窗5開始出售 北京–>贛州 的票… 視窗6開始出售 北京–>贛州 的票… 視窗7開始出售 北京–>贛州 的票… 視窗8開始出售 北京–>贛州 的票… 視窗1出售第500張票,出票成功!剩餘499張票. 視窗1出售第499張票,出票成功!剩餘498張票. 視窗6出售第498張票,出票成功!剩餘497張票. 視窗6出售第497張票,出票成功!剩餘496張票. 視窗1出售第496張票,出票成功!剩餘495張票. 視窗1出售第495張票,出票成功!剩餘494張票. 視窗1出售第494張票,出票成功!剩餘493張票. 視窗2出售第493張票,出票成功!剩餘492張票. 視窗2出售第492張票,出票成功!剩餘491張票. 視窗2出售第491張票,出票成功!剩餘490張票. 視窗2出售第490張票,出票成功!剩餘489張票. 視窗2出售第489張票,出票成功!剩餘488張票. 視窗2出售第488張票,出票成功!剩餘487張票. 視窗6出售第487張票,出票成功!剩餘486張票. 視窗6出售第486張票,出票成功!剩餘485張票. 視窗3出售第485張票,出票成功!剩餘484張票. …… |
在上面的例子中,涉及到資料的更改的Service類saleTicket方法和TicketSaler類run方法都用了synchronized同步鎖進行同步處理,以保證資料的準確性和原子性。
關於synchronized更詳細的用法請參見:《Java中Synchronized的用法》
執行緒控制
在多執行緒程式中,除了最重要的執行緒同步外,還有其它的執行緒控制,如執行緒的中斷、合併、優先順序等。
執行緒等待(wait、notify、notifyAll)
- Wait:使當前的執行緒處於等待狀態;
- Notify:喚醒其中一個等待執行緒;
- notifyAll:喚醒所有等待執行緒。
詳細用法參見:《 Java多執行緒中wait, notify and notifyAll的使用》
執行緒中斷(interrupt)
在Java提供的執行緒支援類Thread中,有三個用於執行緒中斷的方法:
- public void interrupt(); 中斷執行緒。
- public static boolean interrupted(); 是一個靜態方法,用於測試當前執行緒是否已經中斷,並將執行緒的中斷狀態 清除。所以如果執行緒已經中斷,呼叫兩次interrupted,第二次時會返回false,因為第一次返回true後會清除中斷狀態。
- public boolean isInterrupted(); 測試執行緒是否已經中斷。
【Demo4】:執行緒中斷的應用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * 列印執行緒 */ class Printer implements Runnable { public void run() { while (!Thread.currentThread().isInterrupted()) { //如果當前執行緒未被中斷,則執行列印工作 System.out.println(Thread.currentThread().getName() + "列印中… …"); } if (Thread.currentThread().isInterrupted()) { System.out.println("interrupted:" + Thread.interrupted()); //返回當前執行緒的狀態,並清除狀態 System.out.println("isInterrupted:" + Thread.currentThread().isInterrupted()); } } } |
呼叫程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
Printer printer = new Printer(); Thread printerThread = new Thread(printer, "列印執行緒"); printerThread.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("有緊急任務出現,需中斷列印執行緒."); System.out.println("中斷前的狀態:" + printerThread.isInterrupted()); printerThread.interrupt(); // 中斷列印執行緒 System.out.println("中斷前的狀態:" + printerThread.isInterrupted()); |
結果:
1 2 3 4 5 6 7 8 9 10 |
列印執行緒列印中… … … … 列印執行緒列印中… … 有緊急任務出現,需中斷列印執行緒. 列印執行緒列印中… … 中斷前的狀態:false 列印執行緒列印中… … 中斷前的狀態:true interrupted:true isInterrupted:false |
執行緒合併(join)
所謂合併,就是等待其它執行緒執行完,再執行當前執行緒,執行起來的效果就好像把其它執行緒合併到當前執行緒執行一樣。其執行關係如下:
圖 4:執行緒合併的過程
public final void join()
等待該執行緒終止public final void join(long millis);
等待該執行緒終止的時間最長為 millis 毫秒。超時為 0 意味著要一直等下去。public final void join(long millis, int nanos)
等待該執行緒終止的時間最長為 millis 毫秒 + nanos 納秒
這個常見的一個應用就是安裝程式,很多大的軟體都會包含多個外掛,如果選擇完整安裝,則要等所有的外掛都安裝完成才能結束,且外掛與外掛之間還可能會有依賴關係。
【Demo5】:執行緒合併
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/** * 外掛1 */ class Plugin1 implements Runnable { @Override public void run() { System.out.println("外掛1開始安裝."); System.out.println("安裝中..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("外掛1完成安裝."); } } /** * 外掛2 */ class Plugin2 implements Runnable { @Override public void run() { System.out.println("外掛2開始安裝."); System.out.println("安裝中..."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("外掛2完成安裝."); } } |
合併執行緒的呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 |
System.out.println("主執行緒開啟..."); Thread thread1 = new Thread(new Plugin1()); Thread thread2 = new Thread(new Plugin2()); try { thread1.start(); //開始外掛1的安裝 thread1.join(); //等外掛1的安裝執行緒結束 thread2.start(); //再開始外掛2的安裝 thread2.join(); //等外掛2的安裝執行緒結束,才能回到主執行緒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主執行緒結束,程式安裝完成!"); |
結果如下:
1 2 3 4 5 6 7 8 |
主執行緒開啟… 外掛1開始安裝. 安裝中… 外掛1完成安裝. 外掛2開始安裝. 安裝中… 外掛2完成安裝. 主執行緒結束,程式安裝完成! |
優先順序(Priority)
執行緒優先順序是指獲得CPU資源的優先程式。優先順序高的容易獲得CPU資源,優先順序底的較難獲得CPU資源,表現出來的情況就是優先順序越高執行的時間越多。
Java中通過getPriority和setPriority方法獲取和設定執行緒的優先順序。Thread類提供了三個表示優先順序的常量:MIN_PRIORITY優先順序最低,為1;NORM_PRIORITY是正常的優先順序;為5,MAX_PRIORITY優先順序最高,為10。我們建立執行緒物件後,如果不顯示的設定優先順序的話,預設為5。
【Demo】:執行緒優先順序
1 2 3 4 5 6 7 8 9 10 11 |
/** * 優先順序 */ class PriorityThread implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i ++) { System.out.println(Thread.currentThread().getName() + ": " + i); } } } |
呼叫程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
//建立三個執行緒 Thread thread1 = new Thread(new PriorityThread(), "Thread1"); Thread thread2 = new Thread(new PriorityThread(), "Thread2"); Thread thread3 = new Thread(new PriorityThread(), "Thread3"); //設定優先順序 thread1.setPriority(Thread.MAX_PRIORITY); thread2.setPriority(8); //開始執行執行緒 thread3.start(); thread2.start(); thread1.start(); |
從結果中我們可以看到執行緒thread1明顯比執行緒thread3執行的快。