多線程

進程

進程:是系統進行資源分配和調度的一個獨立單位。所有運行中的任務通常對應一條進程,並且進程具有一定的獨立功能。

進程的特徵:

(1)獨立性:進程是系統中獨立存在的實體,它可以擁有自己獨立的資源,每個進程都擁有自己私有的地址空間,在沒有經過進程本身允許的情況下,一個用戶進程不可以直接訪問其他進程的地址空間。

(2)動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。在進程中加入了時間的概念。進程具有自己的生命週期和各種不同的狀態,這些概念在程序中都是不具備的。

(3)併發性:多個進程可以在單個處理器上併發執行,多個進程之間不會互相影響。

併發性和並行性是兩不同的概念,並行指在同一時刻,有多條指令在多個處理器上同時執行;併發指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。


線程:線程是進程的執行單元,就像進程在操作系統中的地位一樣,線程在程序中是獨立的,併發的執行流。一個進程可以有多個線程,但一個線程只能有一個父進程。線程可以有自己的堆棧,自己的程序計數器,自己的局部變量,但不能擁有系統資源,它與父進程的其他線程共享該進程擁有的全部資源。線程可以完成一定的任務,可以其他線程共享父進程中的共享變量及部分環境,相互之間協同完成進程所有的任務。線程的執行是搶佔式的,簡單的說,當前運行的線程在任何時候都有可能被掛起,讓其他線程可以運行。線程的調度和管理是由進程來完成。

多線程:簡單的理解就是同一個應用程序運行時,內部可能包含多個順序執行流,每個順序執行流就是一個線程。

和進程相比,多線程的優點:

(1)進程間不能共享內存,但線程之間共享內存非常容易。

(2)系統創建進程需要爲該進程重新分配系統資源,創建線程的代價就小很多,因此使用多線程來實現多任務併發比多進程的效率高。

(3)Java語言內置多線程功能支持,而不是單純的作爲底層操作系統的調度方式,從而簡化了Java多線程編程。


繼承Thread類創建線程和啓動

Java使用Thread類代表線程,所有的線程對象都必須是Thread類或其子類的實例。Thread類中提供了一個run方法,該方法體就是代表線程需要完成的任務。因此,我們把run方法稱爲線程的執行體。

格式:   public   class  subThread   extends  Thread{

pubilc  void run{

線程執行體

};

}

線程對象的其他常用的方法

(1)start():該方法是Thread的實例方法,啓動調用該方法的線程。

(2)Thread.currentThread():currentThread是Thread類的靜態方法,該方法總是返回當前正在運行的現在對象。

(3)getName():該方法是Thread的實例方法,該方法返回調用方法的線程名字。

注意:使用繼承Thread類的方法來創建線程類,多條線程之間無法共享線程類的實例變量。


實現Runnable接口創建線程類

使用Runnable接口來創建並啓動多線程的步驟:

(1)定義Runnable接口的實現類,並重寫該類接口的run方法,該run方法的方法體是該線程的線程執行體。

(2)創建Runnable實現類的實例,並將此實例作爲Thread的taget類創建Thread對象,該Thread對象纔是真正的線程對象。

(3)調用線程對象的start方法來啓動該線程。

經典實例:

public class RunnableTest implements Runnable {

	private int i;
	@Override
	public void run() {
		for (; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
		}

	}
	public static void main(String[] args) {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + "  " + i);
			if (i == 20) {
				RunnableTest rt = new RunnableTest();
				//通過new Thread(target,name)方法創建新線程
				new Thread(rt,"線程1").start();
				new Thread(rt,"線程2").start();
			}
		}
	}

}

打印部分結果:

線程1 84
線程1 85
線程1 86
main  94
main  95
main  96
線程2 87
線程2 88
線程2 89
解析:根據打印的結果發現,兩條子線程的i變量是連續的,也就是採用Runnable接口的方式創建多條線程可以共享該線程類的實例屬性。這是因爲這樣,程序所創建的Runnable對象只是線程的target,而多態線程可以共享同一個target,所以多條線程可以共享同一個線程類的實例屬性。


兩種方式創建線程對比

採用實現Runnable接口方式的多線程

(1)線程類只是實現了Runnable接口,還可以繼承其他類。

(2)可以多個線程共享同一個target對象。

(3)編程稍微複雜一點,如果要訪問當前線程,必須使用Thread.currentThread()方法。

採用繼承Thread類的方式創建多線程

(1)線程類已經繼承了Thread類,所以不能再繼承其他類。

(2)編寫簡單,如果需要訪問當前線程,無須使用Thread.currentThread()方法,直接使用this即可獲得當前線程。


線程的生命週期:

線程有五個狀態,分別是:新建,就緒,運行,阻塞,死亡五種狀態。線程啓動以後,不能一直霸佔CPU獨自運行,CPU需要在多條線程之間切換,所有線程狀態也會多次在運行,阻塞之間切換。


新建:當程序使用new關鍵字創建一個線程之後,該線程就處於新建狀態。此時僅僅是有Java虛擬機爲其分配了內存,並初始化。

就緒:當線程對象調用了start()方法後,該線程處於就緒狀態。虛擬機會爲其創建方法調用棧和程序計數器,處於這個狀態的線程並沒有開始運行,它只是表示該線程可以運行了,至於什麼時候開始運行,取決於虛擬機裏線程調度器的調度。

注意:啓動線程使用的start方法,而不是run方法,不要調用線程對象的run方法,調用start方法啓動線程,系統會把該run方法當成線程執行體來處理。但如果直接調用線程對象的run方法,則run方法立即就會被執行,而且在run方法返回之前其他線程無法併發執行,也就是說系統把線程對象當成了普通的方法對象,而run方法也是一個普通的方法,而不是線程執行體。

運行:處於就緒狀態的線程獲得了CPU,開始執行run方法的線程執行體。

阻塞:(1)線程調用sleep方法主動放棄所佔用的處理器資源。

             (2)線程調用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞。

             (3)線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。

             (4)線程在等待某個通知。

            (5)程序調用了線程的suspend方法將該線程掛起,此方法容易導致死鎖,所以不建議使用該方法。

死亡:run方法執行完成,線程正常結束;線程拋出了一個未捕獲的Exception或Error;直接調用該線程stop()方法來結束該線程,該方法容易導致死鎖,不建議使用。


注意:Java程序運行時默認的主線程,main方法的方法體就是主線程的線程執行體。當主線程結束時,其他線程不受任何影響,並不會隨之結束。

不要對處於死亡狀態的線程調用start方法,否則會報IllegalThreadStateException異常。表明死亡狀態的線程無法再次運行。可以調用isAlive()方法判斷線程是否死亡。


控制線程

join線程:Thread提供了讓一個線程等待另一個線程完成的方法,join方法。當在某個程序執行流中調用其他線程的join()方法時,調用線程將被阻塞,直到被join方法加入的join線程完成爲止。


後臺線程

運行在後臺,爲其他線程提供服務,這樣的線程叫後臺線程,也叫守護線程,精靈線程等。虛擬機的垃圾回收線程就是典型的後臺線程。

後臺進程的特徵:如果所有的前臺線程都死亡了,後臺線程會自動死亡。

調用Thread對象的setDaemon(true)方法可以將指定線程設置爲後臺線程。

Thread類的isDaemon()方法用於判斷指定的線程是否爲後臺線程。

前臺線程創建的子線程默認是前臺線程,後臺線程創建的子線程默認是後臺線程。

前臺線程死亡後,JVM會通知後臺線程死亡,但從它接受指令,到它做出響應,需要一定時間。而且需要將某個線程設置爲後臺線程,必須在該線程啓動之前設置,也就是說setDaemom(true)必須在start()方法之前調用。


線程休眠:sleep

sleep讓正在運行的線程暫停一段時間,並進入阻塞狀態。

sleep方法有兩種重載的形式:

(1)static  void  sleep(long millis):讓當前正在執行的線程暫停millis毫秒,並進入阻塞狀態,該方法受系統計時器和線程調度器的精度和準確度的影響。

(2)static  void  sleep(long mills,int nanos):讓當前正在執行的線程暫停millis毫秒加nanos毫微秒,並進入阻塞狀態。一般很少調用該方法。

噹噹前線程調用sleep方法進入阻塞狀態後,在其sleep時間內,該線程是不會獲得執行的機會,即使系統中沒有其他可運行的線程,處於sleep中的線程也不會運行。


線程讓步:yield

yield()方法是一個和sleep方法有點相似的方法,它也是一個Thread類提供的一個靜態方法,它也可以讓當前正在執行的線程暫停,但他不會阻塞該線程,它只是將該線程注入就緒狀態。yield只是讓當前線程暫停一下,讓系統的線程調度器重新調度一次。

sleep方法和yield方法的區別:

(1)sleep方法暫停當前線程後,會給其他線程執行機會,不會理會其他線程的優先級。但yield方法只會給優先級相同或者更高的線程執行機會。

(2)sleep方法會將線程轉入阻塞狀態,直到經過阻塞時間纔會轉入就緒狀態。而yield不會將程序轉入阻塞狀態,它只是強制當前線程進入就緒狀態。因此完全有可能某個線程調用yield方法暫停之後,立即再次獲得處理器資源被執行。

(3)sleep方法聲明拋出了InterruptedException異常,而yield方法則沒有拋出任何異常。

(4)sleep方法比yield方法有更好的可移植性,通常不要依靠yield來控制併發線程的執行。


線程的優先級

每個線程執行時都具有一定的優先級,優先級高的線程獲得較多的執行機會,而優先級低的線程則獲得較少的執行機會。

每個線程默認的優先級都與創建它的父線程具有相同的優先級,在默認情況下,main線程具有普通優先級,因此由main線程創建的子線程也有普通優先級。

Thread提供了setPriority(int newPriority)和getPriority()方法來設置和返回指定線程的優先級,其中setPriority方法的參數可以是一個1到10整數。

Thread類提供了三個靜態常量:

(1)MAX_PRIORITY:值是10。

(2)MIN_PRIORITY:值是1。

(3)NORM_PRIORITY:值是5。


線程同步

Java多線程支持引入同步監視器來解決多個線程同時操作一個文件的問題,使用同步監視器的通用方法就是同步代碼塊。

同步代碼塊的語法格式:

synchronized(obj){
需要同步的代碼塊

}

synchronized後括號中的obj就是同步監視器,線程開始執行同步代碼塊之前,必須先獲得對同步監視器的鎖定。任何時候只能有一條線程可以獲得對同步監視器的鎖定,當同步代碼塊執行結束後,該線程自然釋放對該同步監視器的鎖定。

雖然Java程序允許使用任何對象來作爲同步監視器,同步監視器的目的是爲了阻止多條線程同時對一個共享資源進行併發訪問。因此通常推薦使用可能被併發訪問的共享資源充當同步監視器。


同步方法

與同步代碼塊對應的,Java的多線程安全支持還提供了同步方法,同步方法就是使用synchronized關鍵字修飾某個方法。則該方法稱爲同步方法。對於同步方法而言,無須顯示指定同步監視器,同步方法的同步監視器是this,也就是該對象本身。

格式:public  synchronized  void  methodName(){

同步代碼塊

}


釋放同步監視器的鎖定情況:

(1)當前線程的同步方法、同步代碼塊執行結束,當前線程即釋放同步監視器。

(2)當線程在同步代碼塊,同步方法中遇到break、return終止了該代碼塊、該方法的繼續執行,當前線程將會釋放同步監視器。

(3)當線程在同步代碼塊、同步方法中出現了未處理的Error或Exception,導致了該代碼塊,該方法異常結束時將釋放同步監視器。

(4)當線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait()方法,則當前線程暫停,並釋放同步監視器。

在下面兩種情況下面,不會釋放同步監視器

(1)線程執行同步代碼塊或同步方法時,程序調用Thread.sleep()、Thread.yield()方法來暫停當前線程的執行,當前線程不會釋放同步監視器。

(2)線程執行同步代碼塊時,其他線程調用了該線程的suspend方法將該線程掛起,該線程不會釋放同步監聽器。

同步方法或同步代碼塊使用與競爭資源相關的,隱式的同步監視器,並且強制要求加鎖和釋放鎖要出現在一個塊機構中,而且當獲取了多個鎖時,它們必須以相反的順序釋放,且必須在與所有鎖被獲取時相同的範圍內釋放所有鎖。


同步鎖(Lock)

Lock提供了比synchronized方法和synchronized代碼塊更廣泛的鎖定操作,Lock實現允許更靈活的結構,可以具有差別很大的屬性,並且可以支持多個相關的Condition對象。

Lock是控制多個線程對共享資源進行訪問的工具,通常,鎖提供了對共享資源的獨佔訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象。不過,某些鎖可能允許對共享資源併發訪問,如ReadWriteLock(讀寫鎖)。在實現線程安全的控制中,通常使用ReentrantLock(可重入鎖)。使用該Lock對象可以顯示的加鎖和釋放鎖。

Lock對象的代碼格式:

public class LockTest {

	// 定義鎖對象
	private final ReentrantLock lock = new ReentrantLock();

	public void method() {
		// 加鎖
		lock.lock();
		try {
			// 需要保證線程安全的代碼
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			lock.unlock();
		}
	}
}


ReentrantLock鎖具有重入性,也就說線程可以對它已經加鎖的ReentrantLock鎖再次加鎖。ReentrantLock對象會維持一個計數器來追蹤lock方法的嵌套調用,線程在每次調用lock加鎖後,必須顯示調用unlock來釋放鎖,所以一段被鎖保護的代碼可以調用另一個被相同鎖保護的方法。

死鎖

當兩個線程相互等待對方釋放同步監視器時就會發生死鎖。一旦出現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是所有線程處於阻塞狀態,無法繼續。


線程通信

線程協調運行

爲了讓線程協調運行,藉助Object類提供的wait()、notify()和notifAll()三個方法。這三個方法不屬於Thread類,而屬於Object類。但這三個方法必須由監視器對象來調用。

(1)對於使用synchronized修飾的同步方法,因爲該類的默認實例(this)就是同步監視器,所以可以在同步方法中直接調用這三個方法。

(2)對於使用synchronized修飾的同步代碼塊,同步監視器是synchronized後括號裏的對象,所以必須使用該對象調用這三個方法。

Object類提供的三個方法解釋:

(1)wait():導致當前線程等待,直到其他線程調用該同步監視器的notify()方法或notifyAll()方法來喚醒該線程。

(2)notify():喚醒在此同步監視器上等待的單個線程。如果所有線程都在此同步監視器上等待,則會選擇喚醒其中一個線程。選擇是任意性的。只有當前線程放棄對該同步監視器的鎖定後,纔可以執行被喚醒的線程。

(3)notifyAll():喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定後,纔可能執行被喚醒的線程。


使用條件變量控制協調

如果程序不使用synchronized關鍵字來保證同步,而是直接使用Lock對象來保證同步,則系統中不存在隱式的同步監視器對象,也就不能使用wait()、notify()和notifAll()三個方法來協調進程的運行。當使用Lock對象來保證同步時,Java提供了一個Condition類保存協調,使用Condition可以讓那些已經得到Lock對象卻無法繼續執行的線程釋放Lock對象,Condition對象也可以喚醒其他處於等待的線程。

Condition實例實質上被綁定在一個Lock對象上。要獲得特定Lock實例的Condition實例,調用Lock對象newCondition()方法即可。

Condition類提供如下三個方法:

(1)await():類似於隱式同步監視器上的wait方法,導致當前線程等待,直到其他線程調用該Condition的signal()方法或signalAll()方法來喚醒該線程。

(2)signal():喚醒在此Lock對象上等待的單個線程。如果所有線程都在該Lock對象上等待,則會選擇值喚醒其中一個,選擇是任意性的,只有當前線程放棄對該Lock對象的鎖定後,纔可以執行被喚醒的線程。

(3)signalAll():喚醒在此Lock對象上等待的所有線程。只有當前線程放棄對該Lock對象的鎖定後,纔可以執行被喚醒的線程。


Callable和Future

可以認爲Callable接口是Runnable接口的爭強版,Callable接口提供了一個call()方法可以作爲線程的執行體,但call方法比run方法更強大,call方法可以有返回值,可以聲明拋出異常。

Future接口代表Callable接口中call方法的返回值,併爲Future接口提供了一個FutureTask實現類,該實現類實現了Future接口,並實現了Runnable接口-----可以作爲Thread類的target。

Future接口中定義的幾個方法:

(1)boolean  cancel(boolean mayInterruptIfRunning):試圖取消該Future裏關聯的Callable任務。

(2)V  get():返回Callable任務裏call()方法的返回值。調用該方法將導致程序阻塞,必須等到子線程結束時纔會得到返回值。

(3)V  get(long timeout,TimeUnit   unit):返回Callable任務裏call()方法的返回值,該方法讓程序最多阻塞timeout和unit指定的時間。如果經指定時間後Callable任務依然沒有返回值,將拋出TimeoutException異常。

(4)boolean  isCancelled():如果Callable任務正常完成前被取消,則返回true。

(5)boolean  isDone():如果Callable任務已完成,則返回true。

總結創建、並啓動有返回值的線程步驟:

(1)創建Callable接口的實現類,並實現call方法。

(2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象call()方法的返回值。

(3)使用FutureTask對象作爲Thread對象的target創建,並啓動新線程。

(4)調用FutureTask對象的方法來獲取子線程執行結果後的返回值。

經典實例:

public class CallableTest {

	public static void main(String[] args) {
		// 創建Callable對象
		CallableThread ct = new CallableThread();
		// 使用FutureTask來包裝Callable對象
		FutureTask<Integer> task = new FutureTask<Integer>(ct);
		for (int i = 0; i < 100; i++) {
			System.out
					.println(Thread.currentThread().getName() + "   i的值:" + i);
			if (i == 20) {
				// 實質還是以Callable對象來創建、並啓動線程
				new Thread(task, "callable線程").start();
			}
		}
		try {
			// 獲取線程返回值
			System.out.println("子線程的返回值:" + task.get());
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
}

class CallableThread implements Callable<Integer> {

	// 實現call方法,作爲線程的執行體
	@Override
	public Integer call() throws Exception {
		// TODO Auto-generated method stub
		int i = 0;
		for (; i < 100; i++) {
			System.out
					.println(Thread.currentThread().getName() + "   i的值:" + i);
		}
		return i;
	}

}

線程池

線程池在系統啓動時即創建大量空閒的線程,程序將一個Runnable對象傳給線程池,線程池就會啓動一條線程來執行該對象的run方法,當run方法執行結束後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個Runnable對象的run方法。

使用線程池可以有效地控制系統中併發線程的數量,當系統中包含大量併發線程時,會導致系統性能急劇下降,甚至導致虛擬機崩潰。

使用Executors工廠類產生線程池,該類的靜態方法如下

(1)newCachedThreadPool():創建一個具有緩存功能的線程池,系統根據需要創建線程,這些線程將會被緩存在線程池中。

(2)newFixedThreadPool(int  nThreads):創建一個可重用、具有固定線程數的線程池。

(3)newSingleThreadExecutor():創建一個只有單線程的線程池,相當於newFixedThreadPool(1)。

(4)newScheduledThreadPool(int   corePoolSize):創建具有指定線程數的線程池,它可以在指定延遲後執行線程任務。corePoolSize指池中保存的線程數,即使線程是空閒的也被保存在線程池內。

(5)newSingleThreadScheduledExecutor():創建只有一條線程的線程池,它可以在指定延遲後執行線程任務。

上面五個方法中前三個方法返回一個ExecutorService對象,該對象代表一個線程池,它可以執行Runnable對象和Callable對象所代表的線程。而後兩個方法返回一個ScheduledExecutorService線程池,它是ExecutorService的子類,它可以在指定延遲後執行線程任務。

ExecutorService提供如下三個方法

(1)Future<?>  submit(Runnable  task):將一個Runnable對象提交給指定的線程池。線程池將在有空閒線程時執行Runnable對象代表的任務。

(2)<T>   Future<T>  submit(Runnable task,T  result):將一個Runnable對象提交給指定的線程池,線程池將在有空閒線程時執行Runnable對象代表的任務,rusult顯示指定線程執行結束後的返回值。

(3)<T>   Future<T>  submit(Callable<T>  task):將一個Callable對象提交給指定的線程池。線程池將在有空閒線程時執行Callable對象代表的任務,Future代表Callable對象裏call方法的返回值。

ScheduledExecutorService代表可在指定延遲或週期性執行線程任務的線程池。提供如下四個方法:

(1)ScheduledFuture<V>  schedule(Callable<V>  callable,long  delay, TimeUnit  unit):指定callable任務將在delay延遲後執行。

(2)ScheduledFuture<?>  schedule(Runnable command,long  delay, TimeUnit  unit):指定command任務將在delay延遲後執行。

(3)ScheduledFuture<?>  scheduleAtFixedRate(Runnable commed,long  initialDelay,long period,  TimeUnit  unit):指定command任務將在delay延遲後執行,而且以設定頻率重複執行,也就是說,在initialDelay後開始執行,依次在initialDelay+period、initialDelay+2*period、.....處重複執行,以此類推。

(4)ScheduledFuture<?>  scheduleWithFixedDelay(Runnable  commed,long initialDelay,long  delay,TimeUnit  unit) :創建並執行一個在給定初始延遲後首次啓用的定期操作,隨後,在每次執行終止和下一次執行開始之間都存在給定的延遲。如果任務的任一次執行時遇到異常,就會取消後續執行。否則,只能通過程序來顯示取消或終止來終止該任務。

當用完一個線程池後,應該調用該線程池的shutdown()方法,該方法將啓動線程池關閉序列,調用了shutdown方法後的線程池不再結束新任務,但會將以前所有已經提交任務執行完成。當線程池中的所有任務都執行完成後,池中所有線程都會死亡;另外也可以調用線程池的shutdownNow()方法來關閉線程池,該方法試圖停止所有正在執行的活動任務,暫停處理正在等待的任務,並返回等待執行的任務列表。

總結使用線程池來執行線程任務步驟:

(1)調用Executors累的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池。

(2)創建Runnable實現類或Callable實現類的實例,作爲線程執行任務。

(3)調用ExecutorService對象的submit方法來提交Runnable實例或Callable實例。

(4)當不想提交任何任務時,調用ExecutorService對象的shutdown方法來關閉線程池。

經典實例:

public class ThreadTest {
	public static void main(String[] args) {
		// 創建一個具有固定線程數6的線程池
		ExecutorService pool = Executors.newFixedThreadPool(6);
		// 向線程池中提交兩個線程
		pool.submit(new TestThread());
		pool.submit(new TestThread());
		// 關閉線程池
		pool.shutdown();
	}

}

// Runnale接口來實現一個簡單的線程類
class TestThread implements Runnable {

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName() + "的i值爲=" + i);
		}

	}
}


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