Java開發筆記(九十六)線程的基本用法

每啓動一個程序,操做系統的內存中一般會駐留該程序的一個進程,進程包含了程序的完整代碼邏輯。一旦程序退出,進程也就隨之結束;反之,一旦強行結束進程,程序也會跟着退出。普通的程序代碼是從上往下執行的,遇到分支語句則進入知足條件的分支,遇到循環語句總有跳出循環的時候,遇到方法調用則調用完畢仍然返回原處,以後繼續執行控制語句或者方法調用下面的代碼。總之一件事情接着一件事情處理,前一件事情處理完了才能處理後一件事情,這種運行方式被稱做「串行處理」。串行處理的代碼結構清晰,但同一時刻只能執行某段代碼,也就是說,只要一個CPU就足夠應付了。但如今無論電腦仍是手機,中央處理器都是多核CPU,一個設備上集成了四個或更多的CPU,而串行處理的程序自始至終都只用一個CPU,顯然沒法發揮多核CPU的性能優點。既然串行存在效率問題,就須要另外一種容許同時執行多項任務的處理方式,該方式被稱做「並行處理」。所謂並行處理,指的是程序在同一時刻進行不止一個事務的處理,好比看網絡視頻時一邊下載一邊播放,這樣就能提升程序的運行效率。
並行處理的思想體現到程序調度上面,又有多進程與多線程兩種方式,多進程彷彿孫悟空拔毫毛變出許多小孫悟空,每隻小孫悟空都四肢齊全、有鼻子有眼睛,徹底是孫悟空的克隆版本,並且能夠單獨上陣打鬥。至於線程則爲進程中的一條控制流,它是操做系統可以調度的最小執行單元,線程猶如人的手,吃飯穿衣都靠它。多線程彷彿哪吒變出三頭六臂,每隻手臂都拿着一把兵器,戰鬥力頓時倍增。不過變出來的手臂依附於哪吒本人,要是哪吒掛了,再多的手臂也只能拜拜,固然只要進程還在運行,多些線程絕對有助於加快程序的辦事速度。何況一個線程佔用的系統資源遠小於一個進程,想一想看,三個孫悟空有六隻手臂同時佔據了三我的的空間,而三頭六臂的哪吒也有六隻手臂但只佔據一我的的空間,很明顯多線程的性價比要優於多進程。
一個進程默認自帶一個線程,這個默認線程被稱做主線程,要想在主線程以外另外開闢新線程,就用到了Java的Thread線程類。Thread類封裝了線程的生命週期及其調度操做,程序員只需由Thread類派生出新的線程類,並重寫run方法添加具體的業務邏輯便可。下面即是一個計數器線程的代碼例子,功能很簡單,僅僅循環打印0-999的計很多天志:html

	// 定義一個計數器線程
	private static class CountThread extends Thread {
		@Override
		public void run() {
			for (int i=0; i<1000; i++) { // 一千次計數,並打印每次計數的日誌
				// getName方法獲取當前線程的名稱,getId方法獲取當前線程的編號
				PrintUtils.print(getName(), "當前計數值爲"+i);
			}
		}
	}

 

上面代碼在打印日誌時調用了本身寫的print方法,該方法主要打印當前時間、當前線程名稱、具體事件描述等信息,爲節約代碼篇幅,日後的線程內部日誌都經過print方法來打印,如下是該方法的實現代碼:程序員

//定義了線程專用的日誌打印工具
public class PrintUtils {
	// 打印線程的運行日誌,包括當前時間、當前線程名稱、具體事件描述等信息
	public static void print(String threadName, String event) {
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss.SSS");
		String dateTime = sdf.format(new Date());
		String desc = String.format("%s %s %s", dateTime, threadName, event);
		System.out.println(desc);
	}
}

 

定義好了計數器線程,輪到外部啓動它倒也容易,先建立一個計數器線程的對象,再調用該對象的start方法,接着計數器線程便會自動執行run方法的內部代碼。外部啓動計數器線程的調用代碼示例以下:安全

		CountThread thread = new CountThread(); // 建立一個計數器線程
		thread.start(); // 開始線程運行

 

運行上述的調用代碼,觀察到以下的線程運行日誌,可見一個名叫Thread-0的分線程正常跑起來了:網絡

17:36:01.049 Thread-0 當前計數值爲0
17:36:01.051 Thread-0 當前計數值爲1
17:36:01.051 Thread-0 當前計數值爲2
17:36:01.051 Thread-0 當前計數值爲3
………………………這裏省略餘下的日誌……………………

除了start方法,Thread類還提供了其它一些有用的方法。假若程序前後啓動兩個線程,那麼一般來講,先啓動的線程比後啓動的線程要跑得快些。但是有時候業務上又須要後啓動的線程跑得更快,此時可調用指定線程的join方法,該方法字面上的意思是「加入」,實際做用倒是「插隊」。凡是調用了join方法的線程,它們的內部代碼相較其它線程會優先處理,因爲不一樣線程之間是並行展開着的,所以優先的意思並不是必定會插到前面,而是儘可能安排先執行,最終的執行順序還得由操做系統來決定。下面是演示線程插隊功能的代碼例子:多線程

	// 測試線程的插隊操做
	private static void testJoin() {
		 // 建立第一個計數器線程
		CountThread thread1 = new CountThread();
		thread1.start(); // 第一個線程開始運行
		 // 建立第二個計數器線程
		CountThread thread2 = new CountThread();
		thread2.start(); // 第二個線程開始運行
		try {
			thread2.join(); // 第二個線程說:「我很着急,我要插隊」
		} catch (InterruptedException e1) { // 插隊行爲可能會被中斷,須要捕獲中斷異常
			e1.printStackTrace();
		}
	}

 

只有兩個分線程的話,尚能經過join方法區分插隊的線程與普通線程;要是分線程多於兩個,好幾個線程都調用join方法,都提出本線程想插隊,操做系統又該如何伺候這些猴急的線程們?就算是插隊,也得有個插隊順序吧,不是誰嗓門大誰就能排到前面的,因此還需定義一個規矩來區分插隊動做的輕重緩急。因而Thread類又提供了優先級設置方法setPriority,調用該方法便可指定每一個線程的優先級大小,數值越大的表示它擁有越高的優先級,就越應該安排到前面執行。如此一來,經過優先級數值的大小,可以有效辨別各個線程的排隊順序,不再必煩惱要到哪裏插隊了。給多個線程分別設置優先級的代碼示例以下:ide

	// 測試線程的優先級順序
	private static void testPriority() {
		 // 建立第一個計數器線程
		CountThread thread1 = new CountThread();
		thread1.setPriority(1); // 第一個線程的優先級爲1
		thread1.start(); // 第一個線程開始運行
		 // 建立第二個計數器線程
		CountThread thread2 = new CountThread();
		thread2.setPriority(9); // 第二個線程的優先級爲9,值越大優先級越高
		thread2.start(); // 第二個線程開始運行
	}

正常狀況下,分線程的內部代碼執行完畢後,該線程會自動退出運行。但有時須要提早結束線程,或者先暫停線程,等到時機成熟再恢復線程,Thread類也確實提供了相關的處理方法,例如stop方法用於中止線程運行,suspend方法用於暫停線程運行,resume方法用於恢復線程運行。然而Java同時註明了這三個方法都已通過時,爲啥?原因在於它們仨是不安全的,當一個線程正在歡快運行的時候,忽然外部咔嚓一下,不禁分說把它幹翻,這自己就是很危險的舉動,由於誰也沒法預料此時線程在作什麼、線程意外終止會產生什麼後果等等。好比某個線程正在寫文件,如今無論三七二十一干掉該線程,結果極可能形成文件損壞。故而由外部強行干預線程的運行實在不是一個好點子,理想的作法是:外部給分線程發個紙條,表示你被炒魷魚了,咱通情達理也沒馬上趕你走,你收拾收拾差很少了再走也不遲。工具

這樣的話,很天然想到在線程內部增長一個標誌位,分線程每隔一陣子便檢查該標誌,一旦發現標誌位發生改變,就自動擇機退出運行。據此能夠從新編寫包含標誌位的計數器線程,並在run方法中不時地檢查該標誌,新線程的定義代碼示例以下:性能

	// 定義一個主動檢查運行標誌的線程
	private static class ActiveCheckThread extends Thread {
		private boolean canRun = true; // 可否運行的標誌
		// 設置當前線程可否繼續運行的標誌
		public void setCanRun(boolean canRun) {
			this.canRun = canRun;
		}

		@Override
		public void run() {
			for (int i=0; i<1000; i++) {
				PrintUtils.print(getName(), "當前計數值爲"+i);
				if (!canRun) { // 若是不容許運行,就打印中止運行的日誌,並跳出線程的循環處理
					PrintUtils.print(getName(), "主動中止運行");
					break;
				}
			}
		}
	}

上述的線程代碼提供了setCanRun方法給外部調用,經過該方法便可設置當前線程可否繼續運行的標誌。外部在啓動ActiveCheckThread線程以後,再調用setCanRun方法,就實現了給分線程遞紙條的功能。下面是ActiveCheckThread線程的調用代碼例子:測試

	// 線程本身主動檢查是否要中止運行
	private static void testActiveCheck() {
		 // 建立一個會自行檢查運行標誌的線程
		ActiveCheckThread thread = new ActiveCheckThread();
		thread.start(); // 開始線程運行
		try {
			Thread.sleep(50); // 睡眠50毫秒
		} catch (InterruptedException e) { // 睡眠可能會被打斷,須要捕獲中斷異常
			e.printStackTrace();
		}
		thread.setCanRun(false); // 告知該線程不要再跑了,請擇機退出
	}

 

運行上面的測試代碼,觀察到如下的線程日誌,可見分線程按照標誌位提早中止運行了。this

………………………這裏省略前面的日誌……………………
16:38:18.457 Thread-0 當前計數值爲14
16:38:18.457 Thread-0 當前計數值爲15
16:38:18.457 Thread-0 當前計數值爲16
16:38:18.458 Thread-0 主動中止運行

設置標誌位的辦法當然可行,但不是很好用,緣由有二:其一,分線程要很積極主動的去檢查標誌位,但是人算不如天算,標誌位的檢查代碼畢竟不能塞獲得處都是,那麼在遺忘的角落就無法響應外部的信號了;其二,設置標誌位是個新增的方法,那麼每一個線程類的標誌設置方法都不盡相同,外部又怎知甲乙丙丁各自提供了哪些設置方法呢?好在Thread類另外提供了線程中斷機制,分線程倒也沒必要新增可否運行的標誌,原來的代碼結構能夠保持不變。在中斷機制裏,凡是屬於正常的業務邏輯,外部概不橫加干涉,只有在耗時較久的場合,例如睡眠、等待之類的狀況,纔可能會收到中斷信號,也就是中斷異常InterruptedException。因而分線程只管捕捉中斷異常,若無異常則照常運行;如有異常則進入中斷分支,對相關事宜妥善處理一下,便可退出線程運行。

據此改造先前的計數器線程,在每次計數以後增長調用sleep方法,且睡眠期間容許接收中斷信號,另外補充異常處理的try/catch語句,並在異常分支進行善後工做。改造後的計數器線程PassiveInterruptThread代碼示例以下:

	// 定義一個被動接受中斷信號的線程
	private static class PassiveInterruptThread extends Thread {
		@Override
		public void run() {
			try {
				for (int i=0; i<1000; i++) {
					PrintUtils.print(getName(), "當前計數值爲"+i);
					Thread.sleep(10); // 睡眠10毫秒,睡眠期間容許接收中斷信號
				}
			} catch (InterruptedException e) { // 收到了異常中斷的信號,打印中斷日誌並退出線程運行
				PrintUtils.print(getName(), "被中斷運行了");
			}
		}
	}

 

接下來外部啓動計數線程以後,調用interrupt方法往分線程發送中斷信號,注意這個interrupt方法爲Thread類的自有方法,每一個線程都適用。下面是PassiveInterruptThread線程的調用代碼例子:

	// 線程被動接收外部的中斷信號
	private static void testPassiveInterrupt() {
		 // 建立一個會接收外部中斷信號的線程
		PassiveInterruptThread thread = new PassiveInterruptThread();
		thread.start(); // 開始線程運行
		try {
			Thread.sleep(50); // 睡眠50毫秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		thread.interrupt(); // 無論你正在幹什麼,先停下來再說
	}

 

運行上面的線程調用代碼,觀察到以下的線程日誌,可見分線程的確收到了外部的中斷信號:

………………………這裏省略前面的日誌……………………
17:04:33.284 Thread-0 當前計數值爲3
17:04:33.294 Thread-0 當前計數值爲4
17:04:33.304 Thread-0 當前計數值爲5
17:04:33.305 Thread-0 被中斷運行了

  

更多Java技術文章參見《Java開發筆記(序)章節目錄

相關文章
相關標籤/搜索