JAVA多線程

進程和線程

進程

所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中的程序,并且具有一定獨立的功能,進程是系統進行資源分配和調度的一個獨立單位.

進程的特性:? 獨立性? ? ?? ?? 動態性???

并發性:多個進程可以在單個處理器上并發執行,多個進程之間不會相互影響.

并發并行的區別

并行(parellel)指的是在同一時刻,有多條指令在多個處理器上同時被執行;

并發指的是在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,使得宏觀上具有多個進程同時執行的結果.

多線程

多線程擴展了多進程的概念,使得同一進程可以同時并發處理多個任務.線程也被稱為輕量級進程,線程時進程的執行單元.線程在程序中是獨立的并發的執行流.當進程被初始化之后,主線程就被創建了.

線程是進程的組成部分,一個進程可以有多個線程,但一個線程必須有一個父進程.線程可以擁有自己的棧,自己的程序計數器和自己的局部變量,但不擁有系統資源,它與父進程的其他線程共享該進程所擁有的全部資源.因為多個線程共享父進程的所有資源,因此編程比較方便,但必須更加小心,需要確保線程不會妨礙到同一進程里的其他線程.

線程是獨立運行的,它并不知道進程中是否還有其他的線程存在.線程的執行是搶占式的:當前運行的線程在任何時候都可能被掛起,以便另一個線程可以運行.

一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以并發執行.

從邏輯角度來看,多線程存在于一個應用程序中,讓一個應用程序可以有多個執行部分同時進行,但操作系統無須將多個線程看做多個獨立的應用,對多線程實現調度和管理以及資源分配.線程的調度和管理由進程本身負責完成.

總結:

1.操作系統可以同時執行多個任務,每個任務就是進程;

2.進程可以同時執行多個任務,每個任務就是線程.

多線程的優勢

1.進程之間不能共享內存,但線程之間共享內存很容易

2.系統創建進程需要為該進程重新分配系統資源,但創建線程則代價小得多,因此使用多線程來實現多任務并發比多進程的效率高.

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

多線程的應用是很廣泛的,比如一個瀏覽器必須能同時下載多個圖片,一個web服務器必須能同時響應多個用戶請求;Java虛擬機本身就在后臺提供了一個超級線程來進行垃圾回收.....

線程的創建和啟動

Java使用Thread類代表線程,每個線程對象都必須是Thread類或其子類的實例.每個線程的作用是完成一定的任務,實際上是執行一段程序流.

繼承Thread類創建線程類

1.定義Thread類的子類,并重寫該類的run()方法,該run()方法的方法體就代表了線程需要完成的任務.因此把run()方法稱為線程執行體

2.創建Thread子類的實例,即創建了線程對象

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

// 當線程類繼承Thread類時,直接使用this即可獲取當前線程// Thread對象的getName()返回當前該線程的名字// 因此可以直接調用getName()方法返回當前線程的名

// 調用Thread的currentThread方法獲取當前線程

Java程序運行時默認的主線程,main()方法的方法體就是主線程的線程執行體.

可以看到Thread-0和Thread-1兩個線程的輸出的i變量不連續-----注意:i變量是FirstThread的實例變量,而不是局部變量,但是因為程序每次創建線程對象都需要創建一個FirstThread對象,所以Thread-0和Thread-1不能共享該實例變量.

使用繼承Thread類的方法來創建線程類時,多個線程之間是無法共享線程類的實例變量.

實現Runnable接口創建線程類

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

2.創建Runnable實現類的實例,并以此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象

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

// 通過實現Runnable接口來創建線程類

publicclassSecondThreadimplementsRunnable{

?????? private int i ;// run方法同樣是線程執行體

?????? public void run(){for( ; i <100; i++ )? ? ? ? {

?????? ? ? ? ? // 當線程類實現Runnable接口時,// 如果想獲取當前線程,只能用Thread.currentThread()方法。

????????????? System.out.println(Thread.currentThread().getName()? +"? "+ i);? ? ??

??????? }? ???????????


?????? public static void main(String[] args){

????? ? ? ? ? for(inti =0; i <100;? i++)? ? ? ? {??

? ? ? ??????? System.out.println(Thread.currentThread().getName()? +"? "+ i);

????????????? if(i ==20){? ? ? ? ? ? ? ?

?????????????????????? SecondThread st =newSecondThread();

????????????????? // ①//通過創建Runnable實現類的對象SecondThread ,

?????????????????? //以Runnable實現類的對象SecondThread 作為Thread的target來創建Thread對象

???????????????????? // 通過new Thread(target , name)方法創建新線程

???????????????????? newThread(st ,"新線程1").start();newThread(st ,"新線程2").start();? ? ? ? ??

???????????????????????? }? ? ?

? ? ? ? ? ?? }??

????? }

}

當線程類實現Runnable接口時,如果想獲取當前線程,只能用Thread.currentThread()方法

可以看到兩個子線程的i變量是連續的這是因為采用Runnable接口的方式創建的多個線程可以共享線程類的實例變量.是因為:程序創建的Runnable對象只是線程的target,而多個線程可以共享一個target,所以多個線程可以共享一個線程類(實際上應該是線程的target類)的實例變量.

使用Callable和Future創建線程

通過實現Runnable接口創建多線程時,Thread類的作用就是把run()方法包裝成線程執行體.從Java5開始,Java提供了Callable接口,該接口可以理解為是Runnable接口的增強版,Callable接口提供了一個call()方法可以作為線程執行體,但call()方法比run()方法功能更強大,call()方法可以有返回值.call()方法可以聲明拋出的異常.

但是Callable接口并不是Runnable接口的子接口,所以Callable對象不能直接作為Thread的target.而且call()方法還有一個返回值,call()方法并不是直接調用的,它是作為線程執行體被調用的.好在Java提供了Future接口來代表Callable接口里的Call()方法的返回值,并為Future接口提供了一個FutureTask實現類,該實現類既實現了Future接口,并實現了Runnable接口----可以作為Thread類的target.

在Future接口里定義了幾個公共方法來控制它關聯的Callable任務.

Callable接口有泛型限制,并且Callable接口里的泛型形參類型與call()方法返回值類型相同.而且Callable接口是函數式接口,可以用Lambda表達式創建Callable對象

創建并啟動具有返回值的線程的步驟如下:

1.創建Callable接口的實現類,并實現call()方法,該call()方法將作為線程執行體,且該call()方法有返回值,再創建Callable實現類的實例.

2.使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值

3.使用FutureTask對象作為Thread對象的target創建并啟動新線程

4.調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值.

public class ThirdThread {

public static void main(String[] args){

?? // 創建Callable對象

? ThirdThread rt =newThirdThread();

? // 先使用Lambda表達式創建Callable對象

? // 使用FutureTask來包裝Callable對象

? FutureTask task =newFutureTask((Callable)() -> {

??????????????? int i =0;

?????????????? for( ; i <100; i++ )? {

? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()

? ? ? ? ? ? ? ? ? ? +" 的循環變量i的值:"+ i);

? ? ? ? ? ? ? }// call()方法可以有返回值

????????????? return? i;

? ? ? ? });

? for(inti =0; i <100; i++)? ? ? ? {

? ? ? ? ? ? System.out.println(Thread.currentThread().getName()

? ? ? ? ? ? ? ? +" 的循環變量i的值:"+ i);if(i ==20)

? ? ? ? ? {// 實質還是以Callable對象來創建、并啟動線程newThread(task ,"有返回值的線程").start();? ? ? ? ? ? }

? ? ? ? }try{// 獲取線程返回值

????????????? System.out.println("子線程的返回值:"+ task.get());

? ? ? ? }

???????? catch(Exception ex) {

? ? ? ? ? ? ex.printStackTrace();

? ? ? ? }

? ? }

}

創建線程的三種方式對比

采用實現Runnable,Callable接口的方式創建多線程的優缺點:

1.線程類只是實現了Runnable接口或Callable接口,還可以繼承其他類

2.多個線程可以共享同一個target對象,非常適合多個相同線程來處理同一份資源的情況,較好的體現了面向對象的思想

3.需要訪問當前線程,則必須使用Thread.currentThread()方法

采用繼承Thread類的方式創建多線程的優缺點:

1.因為該線程已經繼承了Thread類,所以不能在繼承其他父類

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

線程的生命周期

線程的生命周期中,需要經歷新建(New),就緒(Runnable),運行(Running),堵塞(Blocked),死亡(Dead)5種狀態.

新建和就緒狀態

當程序new關鍵字創建了一個線程之后,該線程就處于新建狀態.此時它和其他java對象一樣,僅僅由java虛擬機為其分配內存,并初始化其成員變量的值,此時的線程對象沒有表現出任何線程的動態特征,程序也不會執行線程的線程執行體了.

線程對象開始執行start()方法之后,該線程就處于就緒狀態,Java虛擬機會為其創建方法調用棧和程序計數器,處于這個狀態中的線程并沒有開始運行,只是表示該線程可以運行了.

注意:啟動線程使用的是start()方法,而不是run()方法!永遠都不要調用線程對象的run()方法!!!調用start()方法來啟動線程,系統會把該run()方法當成線程來處理;如果直接調用線程對象的run()方法,系統會把線程對象當做普通對象來處理,而run()方法也是一個普通方法,而不是線程執行體.

public class InvokeRun extends Thread{

? privateinti ;// 重寫run方法,run方法的方法體就是線程執行體

? public void run(){

??? for( ; i <100; i++ )? ? ? ? {

??? // 直接調用run方法時,Thread的this.getName返回的是該對象名字,

??? // 而不是當前線程的名字。

??? // 使用Thread.currentThread().getName()總是獲取當前線程名字??

????? System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? +" "+ i);// ①

? ? ? }

? ? }

??? public static void main(String[] args){

????? for(inti =0; i <100;? i++)? ? ? ? {

??????? // 調用Thread的currentThread方法獲取當前線程

??????? System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? +" "+ i);

??????? if(i ==20)? ? ? ? ? ? {

??????? // 直接調用線程對象的run方法,

??????? // 系統會把線程對象當成普通對象,run方法當成普通方法,

??????? // 所以下面兩行代碼并不會啟動兩條線程,而是依次執行兩個run方法

??????? newInvokeRun().run();newInvokeRun().run();

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

結果如下(截取部分):

image.png

如果直接調用線程對象的run()方法,則run()方法不能直接通過getName()方法來獲取到當前線程的名字,而是需要使用Thread.currentThread()方法先獲得當前線程,再調用線程對象的getName()方法來獲取線程的名字.

不難看出,啟動線程的正確方法是調用Thread對象的start()方法,而不是直接調用run()方法,否則就變成單線程程序了.

需要指出的是:當調用了線程的run()方法之后,該線程便不再處于新建狀態,不要再次調用線程對象的start()方法.

只能對處于新建狀態的線程調用start()方法,否則將引發IllegalThreadStateException異常

調用線程對象的start()方法之后,該線程立即進入就緒狀態-------就緒狀態相當于"等待執行",但該線程并未真正進入運行狀態.

比如之前我們演示的secondThread那個程序:并不是到20就馬上開啟一個新線程的.

如果希望調用子線程的start()方法后子線程立即開始執行,程序可以使用Thread.sleep(1)來讓當前運行的線程(主線程)睡眠1毫秒----1毫秒就夠了

運行和堵塞狀態

如果處于就緒狀態的線程獲得了CPU,開始執行run()方法的線程執行體,則該線程處于運行狀態.如果計算機只有一個CPU,那么任何時刻都只有一個線程處于運行狀態,如果一個多處理器的機器上,將會有多個線程并行(parallel)執行;當線程數大于處理器數時,依然會存在多個線程在同一個CPU上輪換的現象.

搶占式調度和協作式調度策略

搶占式調度:線代桌面和服務器操作系統一般采取搶占式調度策略,系統會給每個可執行的線程一個小時間段來處理任務;當該時間段用完后,系統就會剝奪該線程所占用的資源,讓其他線程獲得執行的機會.在選擇下一個進程的時候,系統會考慮線程的優先級

協作式調度:小型設備如手機則采取協作式調度策略,只有當一個線程調用了它的sleep()或yeid()方法后才會放棄所占用的資源-----即必須由該線程主動放棄所占用的資源

線程將會進入堵塞狀態

1.線程調用sleep()方法主動放棄所占用的處理器資源

2.線程調用了一個堵塞式IO方法,在該方法返回之前,該線程被堵塞

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

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

5.線程調用了線程的suspend()方法將該線程掛起,這個方法容易引起死鎖(要盡量避免!!!)已過時。該方法已經遭到反對,因為它具有固有的死鎖傾向。如果目標線程掛起時在保護關鍵系統資源的監視器上保持有鎖,則在目標線程重新開始以前任何線程都不能訪問該資源。如果重新開始目標線程的線程想在調用resume之前鎖定該監視器,則會發生死鎖。這類死鎖通常會證明自己是“凍結”的進程。

如果當前線程被堵塞之后,其他線程就可以獲得執行的機會,被堵塞的線程會在合適的時候重新進入就緒狀態,注意是就緒狀態而不是運行狀態.被堵塞線程的堵塞解除后,必須重新等待線程調度器再次調用它.

解除上面的堵塞

1.調用sleep()方法的線程經過了指定的時間

2.線程調用的堵塞式IO方法已經返回

3.線程成功地獲得了試圖取得的同步監視器

4.線程正在等待某個通知時,其他線程發出了一條通知

5.處于掛起狀態的線程被調用了resume()恢復方法

線程狀態轉換圖

不難看出,線程從堵塞狀態進入就緒狀態,無法直接進入運行狀態.而就緒和運行狀態之間的轉換通常不受程序控制,而是由系統線程調度所決定,當處于就緒狀態的線程獲取到CPU的資源時,該線程進入運行狀態;當處于運行狀態的線程失去處理器資源時,該線程進入就緒狀態.? 有一個方法例外:可以調用yield()方法可以讓運行狀態的線程轉入就緒狀態.

線程死亡

線程會以下列三種方式結束,結束后就處于死亡狀態

1.run()或call()方法執行完成,線程正常結束

2.線程拋出一個未捕獲的Exception或Error

3.直接調用該線程的stop()方法來結束該線程-----該方法容易引起死鎖(不推介!!!)

當主線程結束時,其他線程不受任何影響,并不會隨之結束.一旦子線程啟動起來,它就擁有和主線程相同的地位,它不會受主線程的影響

為了測試某個線程是否已經死亡,可以調用該對象的isAlive()方法,當線程處于就緒,運行,堵塞三種狀態時,該方法返回true,當線程處于新建死亡兩種狀態時,該方法將返回false

不要對一個已經死亡的線程再調用start()方法來讓它重新啟動,死亡就是死亡,該線程將不可再次作為線程執行.如果依然對一個已經死亡的線程再次調用start()方法來啟動該線程,將會引發IllegalThreadStateException異常,這表明處于死亡狀態的線程已經無法再次運行了.

如下程序可以說明上述現象:

public class StartDead extends Thread{

? private int i ;// 重寫run方法,run方法的方法體就是線程執行體

? public void run(){

??? for( ; i <100; i++ )? ? ? ? {

? ? ? System.out.println(getName() +" "+ i);

? ?? }

? ? }

? public static void main(String[] args){

??? // 創建線程對象

?? StartDead sd =newStartDead();

??? for(inti =0; i <300;? i++)? ? ? ? {

????? // 調用Thread的currentThread方法獲取當前線程

???? System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? +" "+ i);

??????? if(i ==20)? ? ? ? ? ? {

???????? // 啟動線程

???????? sd.start();

???????? // 判斷啟動后線程的isAlive()值,輸出true

???????? System.out.println(sd.isAlive());

? ? ?? }// 只有當線程處于新建、死亡兩種狀態時isAlive()方法返回false。

?????? // 當i > 20,則該線程肯定已經啟動過了,如果sd.isAlive()為假時,

????? // 那只能是死亡狀態了。

??????? if(i >20&& !sd.isAlive())? ? ? ? ? ? {

?????????? // 試圖再次啟動該線程sd.start();

? ? ? ? ? ? }

? ? ? ? }

? ? }

}

總結:不要對一個已經死亡的線程再調用start()方法,程序只能對新建狀態的線程調用start()方法,對新建狀態的線程兩次調用start()方法也是錯誤的,上述兩種情況都會引發IllegalThreadStateException異常

控制線程

join線程

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

比如下面程序中的mian線程即主線程,主線程中調用了其他線程(jt線程)的join()方法,此時調用線程(main線程)將被堵塞,直到被join()方法加入的join線程執行完畢為止.

join()方法通常由使用線程的程序調用,目的是:將大問題劃分為許多小問題,每個小問題分配一個線程.當所有的小問題都得到解決處理后,再調用主線程來進一步操作.

public class JoinThread extends Thread{

? // 提供一個有參數的構造器,用于設置該線程的名字

? public JoinThread(String name){

??? super(name);

?? }

? // 重寫run()方法,定義線程執行體

? public void run(){

??? for(inti =0; i <100; i++ )? ? ? ? {

? ? ? ? ? ? System.out.println(getName() +"? "+ i);

? ? ? ? }

? }

public static void main(String[] args) throws Exception{

? // 啟動子線程

? newJoinThread("新線程").start();

? for(inti =0; i <100; i++ )? ? ? ? {

? if(i ==20) {? ? ??

? ? ? ? JoinThread jt =new JoinThread("被Join的線程");

? ? ? ?? jt.start();

??????? // main線程調用了jt線程的join()方法,main線程

?????? // 必須等jt執行結束才會向下執行

????? ? jt.join(); ??

? ? ? ? }? ?

? ? ? ? System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? +"? "+ i);? ? ? ? }

? ? }

}

image.png

上述代碼中main線程中調用了jt線程的join()方法,main線程必須要等jt線程執行完畢之后才會向下執行

join()方法有如下三種重載的方法:

1.join():等待被join的線程執行完成.

2.join(long millis):等待被join的線程的時間最長為millis毫秒.

3.join(long millis,int nanos):等待被join的線程的事件最長為millis毫秒加nanos毫微秒(這個方法很少用!!!)

后臺線程

有一種線程,它是在后臺運行的,它的任務是為其他的線程提供服務的,這種線程稱為后臺線程(Daemon Thread).JVM的垃圾回收線程就是典型的后臺線程.

后臺線程的特征:如果所有的前臺線程都死亡,后臺線程自動死亡.

調用Thread對象的setDaemon(true)方法可將指定線程設置成后臺線程.

public class DaemonThread extends Thread{

? // 定義后臺線程的線程執行體與普通線程沒有任何區別

? public void run(){

????? for(int i =0; i <1000; i++ )? ? ? ? {

? ? ? ? ? ? System.out.println(getName() +"? "+ i);

? ? ?? }

? ? }

??? public static void main(String[] args){

? ? ? ? DaemonThread t =new DaemonThread();

??????? // 將此線程設置成后臺線程

??????? t.setDaemon(true);

?????? // 啟動后臺線程

??? ? t.start();

? ? ? for(int i =0; i <10; i++ )? ? ? ? {

? ? ? ? ? ? System.out.println(Thread.currentThread().getName()? +"? "+ i);?

? ? ? }

// -----程序執行到此處,前臺線程(main線程)結束------

// 后臺線程也應該隨之結束

}

}

本來該線程應該執行到i=999才會結束,但運行程序時不難發現該后臺線程無法運行到999,因為當主線程也就是程序中唯一的前臺線程運行結束后,JVM會主動退出,因而后臺線程也就被結束了.

Thread類還提供了一個isDaemon()方法來判斷當前線程是否為后臺線程.

上面程序中:主線程默認是前臺線程,t線程默認是后臺線程.并不是所有的線程默認都是前臺線程,有些線程默認就是后臺線程-----------前臺線程創建的子線程默認是前臺線程,后臺線程創建的子線程默認是后臺線程.

前臺線程死亡之后,JVM會通知后臺線程死亡,但從它接收到指令到做出相應,需要一定時間(這也是為什么上圖中:在main線程死亡之后Thread-0還進行了一會才死亡的原因).而且將某個線程設置為后臺線程,必須要在該線程啟動之前設置,即setDaemon(true)必須在start()方法之前調用,否則會引發IllegalThreadStateException異常

線程睡眠sleep

如果需要讓當前正在執行的線程暫停一段時間,并進入堵塞狀態,則可以通過調用Thread類的靜態sleep()方法來實現.

sleep()方法有兩種重載形式:

1.static void sleep(long millis):讓當前正在執行的線程暫停millis毫秒,并進入堵塞狀態

2.static void sleep(long millis,intnanos):讓當前正在執行的線程暫停millis毫秒加nanos毫微秒,并進入堵塞狀態(很少用)

當前線程調用sleep()方法進入堵塞狀態后,在其睡眠時間段內,該線程不會獲得執行的機會,即使系統中沒有其他可執行的線程,處于sleep()中的線程也不會執行,因此sleep()方法常用來暫停程序的執行.

publicclassSleepTest{publicstaticvoidmain(String[] args)throwsException{for(inti =0; i <10; i++ )? ? ? ? {? ? ? ? ? ? System.out.println("當前時間: "+newDate());// 調用sleep方法讓當前線程暫停1s。Thread.sleep(1000);? ? ? ? }? ? }}

程序依次輸出10條字符串,輸出2條字符串之間的時間間隔為1秒

線程讓步:yeid

yeid()方法也是Thread類提供的一個靜態方法,它也可以讓當前正在執行的線程暫停,但它不會阻塞該線程,它只是讓該線程轉入就緒狀態.yield()只是讓當前線程暫停一下,讓系統的線程調度器重新調度一次.完全可能的情況是:當某個線程調用了yield()方法暫停之后,線程調度器又將其調度出來重新執行.

當某個線程調用了yield()方法暫停之后,只有優先級與當前線程相同,或者優先級比當前線程更高處于就緒狀態的線程才會獲得執行的機會.

publicclassYieldTestextendsThread{publicYieldTest(String name){super(name);? ? }// 定義run方法作為線程執行體publicvoidrun(){for(inti =0; i <50; i++ )? ? ? ? {? ? ? ? ? ? System.out.println(getName() +"? "+ i);// 當i等于20時,使用yield方法讓當前線程讓步if(i ==20)? ? ? ? ? ? {? ? ? ? ? ? ? ? Thread.yield();? ? ? ? ? ? }? ? ? ? }? ? }publicstaticvoidmain(String[] args)throwsException{// 啟動兩條并發線程YieldTest yt1 =newYieldTest("高級");// 將ty1線程設置成最高優先級yt1.setPriority(Thread.MAX_PRIORITY);? ? ? ? yt1.start();? ? ? ? YieldTest yt2 =newYieldTest("低級");// 將yt2線程設置成最低優先級yt2.setPriority(Thread.MIN_PRIORITY);? ? ? ? yt2.start();? ? }}

如果使用多CPU來運行上述程序,可能效果不是很明顯因為并發在多核CPU上效果不明顯單核CPU比較明顯

sleep()和yield()方法的區別

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

2.sleep()方法會使線程進入堵塞狀態,知道經過堵塞時間才會轉入就緒狀態;而yield()不會將線程轉入堵塞狀態,它只是強調當前線程進入就緒狀態.因此完全有可能某個線程調用yield()方法暫停之后,立即重新獲得處理器資源而被執行

3.sleep()方法聲明拋出了InterruptedException異常,所以調用sleep()方法時要么捕捉該異常,要么顯式聲明拋出該異常;而yield()方法則沒有聲明拋出任何異常

4.sleep()方法比yield()方法有更好的移植性,通常不建議用yield()方法來控制并發線程的執行.

改變線程的優先級

每個線程執行都有一定的優先級,優先級越高的線程將獲得較多的執行機會,而優先級低的線程則獲得較少的機會.每個線程默認的優先級都與創建它的父類線程的優先級相同,main線程具有普通優先級,由main線程創建的子線程的優先級也具有普通優先級.

Thread類提供了setPriority(int newPriority),getPriority()方法來設置和返回指定的線程的優先級,setPriority()方法的參數可以是一個整數,范圍是1~10之間,也可以使用如下三個靜態常量:

MAX_PRIORITY:其值是10

MIN_PRIORITY:其值是1

NORM_PRIORITY:其值是5

publicclassPriorityTestextendsThread{// 定義一個有參數的構造器,用于創建線程時指定namepublicPriorityTest(String name){super(name);? ? }publicvoidrun(){for(inti =0; i <50; i++ )? ? ? ? {? ? ? ? ? ? System.out.println(getName() +",其優先級是:"+ getPriority() +",循環變量的值為:"+ i);? ? ? ? }? ? }publicstaticvoidmain(String[] args){// 改變主線程的優先級Thread.currentThread().setPriority(6);for(inti =0; i <30; i++ )? ? ? ? {if(i ==10)? ? ? ? ? ? {? ? ? ? ? ? ? ? PriorityTest low? =newPriorityTest("低級");? ? ? ? ? ? ? ? low.start();? ? ? ? ? ? ? ? System.out.println("創建之初的優先級:"+ low.getPriority());// 設置該線程為最低優先級low.setPriority(Thread.MIN_PRIORITY);? ? ? ? ? ? }if(i ==20)? ? ? ? ? ? {? ? ? ? ? ? ? ? PriorityTest high =newPriorityTest("高級");? ? ? ? ? ? ? ? high.start();? ? ? ? ? ? ? ? System.out.println("創建之初的優先級:"+ high.getPriority());// 設置該線程為最高優先級high.setPriority(Thread.MAX_PRIORITY);? ? ? ? ? ? }? ? ? ? }? ? }}

遺憾的是Java雖然提供了10個優先級,但這10個優先級并不都與操作系統兼容,比如win2000只提供了7個優先級所以盡量避免直接為線程指定優先級,而應該采用MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY三個靜態常量來設置優先級,這樣才能保證程序具有良好的可移植性.

線程同步

由系統的線程調度具有一定的隨機性造成的,不過即使程序偶然出現問題,那也是由于編程不當引起的.當多個線程來訪問同一個數據時,很容易"偶然"出現安全性問題.

線程安全問題:

銀行取錢問題:

因為線程調度具有不確定性,假設系統線程調度器在粗體字代碼處暫停,讓另一個線程執行------為了強制暫停,只要取消上面程序中的粗體字代碼的注釋即可.

同步代碼塊:

因為run()方法的方法體不具有同步安全性------程序中有兩個并發線程在修改Account對象;而且系統恰好在粗體字代碼處執行線程切換,切換給另一個修改Account對象的線程,所以就出現了問題.就跟以前講的文件并發訪問,當有兩個進程并發修改同一個文件時就有可能造成異常.

為了解決上述問題,Java引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊

//synchronized后括號里的obj就是同步監視器synchronized(obj){ ......//此處的代碼就是同步代碼塊}

上述代碼的含義是:在線程開始執行同步代碼塊之前,必須先獲得對同步監視器的鎖定.

任何時刻只能有一個線程可以獲得同步監視器的鎖定,當同步代碼塊執行完成之后,該線程會釋放該同步監視器的鎖定.

同步監視器的目的:阻止兩個線程對同一個共享資源進行并發訪問,推介使用可能被并發訪問的共享資源充當同步監視器

publicclassDrawThreadextendsThread{// 模擬用戶賬戶privateAccount account;// 當前取錢線程所希望取的錢數privatedoubledrawAmount;publicDrawThread(String name , Account account? ? ? ? ,doubledrawAmount){super(name);this.account = account;this.drawAmount = drawAmount;? ? }// 當多條線程修改同一個共享數據時,將涉及數據安全問題。publicvoidrun(){// 使用account作為同步監視器,任何線程進入下面同步代碼塊之前,// 必須先獲得對account賬戶的鎖定——其他線程無法獲得鎖,也就無法修改它// 這種做法符合:“加鎖 → 修改 → 釋放鎖”的邏輯

synchronized(account)? ? ? ? {

// 賬戶余額大于取錢數目

if(account.getBalance() >= drawAmount)? ? ? ? ? ? {

// 吐出鈔票System.out.println(getName() +"取錢成功!吐出鈔票:"+drawAmount);

try{

? ? ? ? ? ? ? ? ? ? Thread.sleep(1);

? ? ? ? ? ? ? ? }catch(InterruptedException ex)? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? ex.printStackTrace();

? ? ? ? ? ? ? ? }

// 修改余額account.setBalance(account.getBalance() - drawAmount);? ? ? ? ? ? ? ? System.out.println("\t余額為: "+ account.getBalance());

? ? ? ? ? }else{? ? ? ? ? ? ? ? System.out.println(getName() +"取錢失敗!余額不足!");? ? ? ? ? ? }

? ? ? }// 同步代碼塊結束,該線程釋放同步鎖}}

這種做法符合"加鎖---修改---釋放鎖"的邏輯,任何線程在修改指定資源之前,首先對該資源加鎖,在加鎖期間其他線程無法修改該資源,當線程修改完成后,該線程釋放對該資源的鎖定.

通過這種方式可以保證并發線程在同一時刻只有一個線程可以進入修改共享資源的代碼區(也被稱為臨界區),所以同一時刻最多只有一個線程處于臨界區內,從而保證了線程的安全性.

同步方法

Java多線程還提供了同步方法來和同步代碼塊相對應,使用synchronized字來修飾某個方法,該方法稱為同步方法.對于synchronized關鍵字修飾的實例方法,無須顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象.

通過同步方法可以非常方便的實現線程安全的類,線程安全的類具有如下特征:

1.該類的對象可以被多個線程安全地訪問

2.每個線程調用該對象的任意方法之后都能得到正確結果

3.每個線程調用該對象的任意方法之后,該對象狀態依然保持合理的狀態

不可變類總是線程安全的,因為它的對象時不可變的;但可變對象需要額外的方法來保證其線程安全.

publicclassAccount{

// 封裝賬戶編號、賬戶余額兩個成員變量

privateString accountNo;

private double balance;

publicAccount(){}// 構造器

publicAccount(String accountNo ,doublebalance){

this.accountNo = accountNo;

this.balance = balance;

? ? }// accountNo的setter和getter方法

public void setAccountNo(String accountNo){

this.accountNo = accountNo;

? ? }publicStringgetAccountNo(){returnthis.accountNo;

? ? }// 因此賬戶余額不允許隨便修改,所以只為balance提供getter方法,

publicdoublegetBalance(){returnthis.balance;

? ? }// 提供一個線程安全draw()方法來完成取錢操作publicsynchronizedvoiddraw(doubledrawAmount){

// 賬戶余額大于取錢數目if(balance >= drawAmount)? ? ? ? {

// 吐出鈔票System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? +"取錢成功!吐出鈔票:"+ drawAmount);

try{

? ? ? ? ? ? ? ? Thread.sleep(1);

? ? ? ? ? ? }catch(InterruptedException ex)? ? ? ? ? ? {

? ? ? ? ? ? ? ? ex.printStackTrace();

? ? ? ? ? ? }// 修改余額balance -= drawAmount;

? ? ? ? ? ? System.out.println("\t余額為: "+ balance);

? ? ? ? }else{

? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +"取錢失敗!余額不足!");

? ? ? ? }

? ? }

// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法

public int hashCode(){

return accountNo.hashCode();

? ? }

publicbooleanequals(Object obj){

if(this== obj)return true;

if(obj !=null&& obj.getClass() == Account.class)? ? ? ? {

? ? ? ? ? ? Account target =(Account)obj;

return target.getAccountNo().equals(accountNo);

? ? ? ? }

return false;

? ? }

}

增加了一個代表取錢的draw()方法,并使用synchronized關鍵字來修飾該方法,把該方法編程同步方法,該同步方法的同步監視器是this,對于同一個Account賬戶而言,任意時刻只能有一個線程獲得對Account對象的鎖定,然后進入draw()方法執行取錢操作-----這樣也可以保證多個線程并發取錢的線程安全.

注意:synvhronized關鍵字可以修飾方法,可以修飾代碼塊,但不能修飾構造器,成員變量等等.

publicclassDrawThreadextendsThread{// 模擬用戶賬戶privateAccount account;// 當前取錢線程所希望取的錢數privatedoubledrawAmount;publicDrawThread(String name , Account account? ? ? ? ,doubledrawAmount){super(name);this.account = account;this.drawAmount = drawAmount;? ? }// 當多條線程修改同一個共享數據時,將涉及數據安全問題。publicvoidrun(){// 直接調用account對象的draw方法來執行取錢// 同步方法的同步監視器是this,this代表調用draw()方法的對象。// 也就是說:線程進入draw()方法之前,必須先對account對象的加鎖。account.draw(drawAmount);? ? }}

在上面的示例中,調用draw()方法的對象是account,多個線程并發修改同一份account之前,必須先對account對象加鎖,這也符合"加鎖---修改---釋放鎖"的邏輯

面向對象中的一種流行的設計模式:

DDD(領域驅動設計):這種方式認為每個類都應該是完備的領域對象,比如:Account代表用戶賬戶,應該提供用戶賬戶的相關方法;通過draw()方法來執行取錢操作(實際上還應該提供transfer()等方法來完成轉賬等操作),而不是直接將setBalance()方法暴露出來任人操作,這樣才能保證Account對象的完整性和一致性.

可變類的線程安全是以降低程序的運行效率作為代價的.

1.不要堆線程安全類的所有方法進行同步,只對那些會改變競爭資源(競爭資源也就是共享資源)的方法進行同步.

2.可變類有兩種運行環境:單線程環境和多線程環境,則應該為該可變類提供兩種版本,即線程不安全版本和線程安全版本.在單線程環境中使用線程不安全版本以保證性能(StringBuilder);在多線程中使用線程安全的版本(StringBuffer)

釋放同步監視器的鎖定

程序無須顯式釋放對同步監視器的鎖定,線程會在如下幾種情況下釋放對同步監視器的鎖定

1.當前線程的同步方法,同步代碼塊執行結束

2.當前線程在同步代碼塊,同步方法中遇到break,return終止了代碼塊導致其異常結束

3.當前線程在同步代碼塊,同步方法中出現了未處理的Error和Exception

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

下面出現的情況,線程不會釋放同步監視器

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

2.線程在執行同步代碼塊時,其他線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放同步監視器.程序應該盡量避免使用suspend()和resume()方法來控制線程.

同步鎖

通過顯式定義同步鎖對象來實現同步-----同步鎖對象由Lock對象充當.(這是一種更為強大的線程同步機制)

Lock是控制多個線程對共享資源進行訪問的工具,每次只能有一個線程對Lock對象加鎖,程序開始訪問共享資源之前首先要先獲得Lock對象

某些鎖可能允許對共享資源并發訪問,如ReadWriteLock(讀寫鎖);

Lock,ReadWriteLock是Java5提供的兩個根接口,并為Lock提供了ReentrantLock(可重入鎖)實現類,為ReadWriteLock提供了ReentrantReadWriteLock實現類

ReentrantReadWriteLock為讀寫提供了三種鎖模式:Writing,ReadingOptimistic,Reading

在實現線程安全的控制中比較常用的是ReentrantLock(可重入鎖).使用該Lock對象可以顯式地釋放鎖,加鎖.

import java.util.concurrent.locks.*;

publicclassAccount{

// 定義鎖對象

privatefinalReentrantLock lock =newReentrantLock();

//.......

// 提供一個線程安全draw()方法來完成取錢操作(定義一個保證線程安全的方法)

public void draw(doubledrawAmount){

// 加鎖

lock.lock();

try{

// 賬戶余額大于取錢數目

if(balance >= drawAmount)? ? ? ? ? ? {

// 吐出鈔票

System.out.println(Thread.currentThread().getName() +"取錢成功!吐出鈔票:"+ drawAmount);

try{

? ? ? ? ? ? ? ?? Thread.sleep(1);

? ? ? ? ? ? ? ? }catch(InterruptedException ex)? ? ? ? ? ? ? ? {? ? ? ? ? ? ? ? ? ? ex.printStackTrace();

? ? ? ? ? ? ? ? }

// 修改余額balance -= drawAmount;

? ? ? ? ? ? ? ? System.out.println("\t余額為: "+ balance);

? ? ? ? ? ? }else{

? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()? +"取錢失敗!余額不足!");

? ? ? ? ? ? }

? ? ? ? }finally{

// 修改完成,釋放鎖lock.unlock();? ? ? ? }

? }// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法

public int hashCode(){

return accountNo.hashCode();

? ? }

public boolean equals(Object obj){

if(this== obj) return true;

if(obj !=null&& obj.getClass() == Account.class)? ? ? ? {

? ? ? ? ? ? Account target = (Account)obj;returntarget.getAccountNo().equals(accountNo);? ? ? ? }return false;? ? }

}

使用ReentrantLock對象來進行同步,加鎖和釋放鎖出現在不同的作用范圍內時,通常建議使用finally塊來確保在必要時釋放鎖.

程序中實現draw()方法時,進入方法開始執行后立即請求對ReentrantLock對象進行加鎖,當執行完draw()方法的取錢邏輯后,程序使用finally塊確保釋放鎖.

使用Lock時是顯式調用Lock對象作為同步鎖,而使用同步方法時系統隱式地使用當前對象作為同步監視器,同樣都符合"加鎖---修改---釋放鎖"的操作模式,而且Lock對象時每個Lock對象都對應一個Account對象,一樣可以保證對于同一個Account對象,同一時刻只能有一個線程能進入臨界區

ReentrantLock鎖具有可重入性,一個線程可以對已加鎖的ReentrantLock鎖再次加鎖,ReentrantLock對象會維持一個計數器來追蹤lock()方法的嵌套使用,線程在每次調用lock()方法加鎖后,必須顯式調用unlock()方法來釋放鎖,所以一段被鎖保護的代碼可以調用另一個被相同鎖保護的方法.

死鎖

當兩個線程互相等待對方釋放同步監視器時就會發生死鎖.Java沒有提供任何檢測措施來處理死鎖的情況,所以多線程編程時應該盡量采取措施來避免死鎖的出現.一旦出現死鎖,整個程序既不會發生任何異常,也不會給出任何提示,只是所有線程處于堵塞狀態,無法繼續.

死鎖很容易發生,尤其是在系統中出現多個同步監視器的情況下:

classA{

public synchronized void foo( B b ){

? ? ? ? System.out.println("當前線程名: "+ Thread.currentThread().getName()? ? ? ? ? ? +" 進入了A實例的foo()方法");

// ①

try{??

? ? ? ? Thread.sleep(200);

? ? ? ? }catch(InterruptedException ex)? ? ? ? {

? ? ? ? ? ? ex.printStackTrace();

? ? ? ? }

? ? ? System.out.println("當前線程名: "+ Thread.currentThread().getName()? ? ? ? ? ? +" 企圖調用B實例的last()方法");

// ③

b.last();

? ? }

public synchronized void last(){

? ? ? ? System.out.println("進入了A類的last()方法內部");

? ? }

}classB{publicsynchronizedvoidbar( A a ){

? ? ? ? System.out.println("當前線程名: "+ Thread.currentThread().getName()? ? ? ? ? ? +" 進入了B實例的bar()方法");

// ②

try{

? ? ? ? ? ? Thread.sleep(200);

? ? ? ? }catch(InterruptedException ex)? ? ? ? {

? ? ? ? ? ? ex.printStackTrace();

? ? ? }? ? ? ? System.out.println("當前線程名: "+ Thread.currentThread().getName()? ? ? ? ? ? +" 企圖調用A實例的last()方法");// ④a.last();? ? }

public synchronized void last(){

? ? ? ? System.out.println("進入了B類的last()方法內部");? ? }

}

public class DeadLock implements Runnable{

? ? A a =newA();

? ? B b =newB();

public void init(){

? ? ? Thread.currentThread().setName("主線程");

// 調用a對象的foo方法a.foo(b);

? ? ? ? System.out.println("進入了主線程之后");

? ? }

public void run(){

? ? ? Thread.currentThread().setName("副線程");

// 調用b對象的bar方法b.bar(a);

? ? ? ? System.out.println("進入了副線程之后");

? ? }

public static void main(String[] args){

? ? ? ? DeadLock dl =newDeadLock();

// 以dl為target啟動新線程

newThread(dl).start();

// 調用init()方法dl.init();? ? }

}

Thread類的suspend()方法也容易導致死鎖,Java不推介使用該方法來暫停線程的執行.

線程通信

程序通常無法準確控制線程的輪換執行,但Java也提供了一些機制來保證線程協調運行.

傳統的線程通信

Object類提供的三個方法(這三個方法必須由同步監視器對象來調用):

同步監視器對象可以分為下列兩種情況:

1.使用synchronized修飾的同步方法,該類的默認實例(this)就是同步監視器.

2.使用synchronized修飾的同步代碼塊,同步監視器是synchronized后括號里的對象

這三個方法解釋如下:

wait():導致當前線程等待,直到其它線程調用該同步監視器的notify()方法或notifyAll()方法來喚醒該線程.調用wait()方法的當前線程會釋放對該同步監視器的鎖定.

notify():喚醒此同步監視器上等待的單個線程.只有當前線程放棄對該同步監視器的鎖定后(使用wait()方法),才可以執行被喚醒的線程.

notifyAll():喚醒在此同步監視器上等待的所有線程.只有當前線程放棄對該同步監視器的鎖定后,才可以執行被喚醒的線程.

public class Account{

// 封裝賬戶編號、賬戶余額的兩個成員變量

private String accountNo;

private double balance;

// 標識賬戶中是否已有存款的旗標

private boolean flag =false;

public Account(){}

// 構造器

public Account(String accountNo ,doublebalance){

this.accountNo = accountNo;this.balance = balance;

? ? }

// accountNo的setter和getter方法

public void setAccountNo(String accountNo){

this.accountNo = accountNo;

? ? }

public String getAccountNo(){

returnthis.accountNo;

? ? }

// 因此賬戶余額不允許隨便修改,所以只為balance提供getter方法,

public double getBalance(){

returnthis.balance;

? ? }

public synchronized void draw(doubledrawAmount){

try{

// 如果flag為假,表明賬戶中還沒有人存錢進去,取錢方法阻塞

if(!flag)? ? ? ? ? ? {

? ? ? ? ? ? ? ? wait();

? ? ? ? ? }else{

????????? // 執行取錢System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? ? ? +" 取錢:"+? drawAmount);

? ? ? ? ? ? ? ? balance -= drawAmount;

? ? ? ? ? ? ? ? System.out.println("賬戶余額為:"+ balance);

???????? // 將標識賬戶是否已有存款的旗標設為false 。

???????????? flag =false;// 喚醒其他線程

???????????? notifyAll();

? ? ? ? ? ? }

? ? ? ? }catch(InterruptedException ex)? ? ? ? {

? ? ? ? ? ? ex.printStackTrace();? ? ? ? }? ? }

public synchronized void deposit(doubledepositAmount){

try{

// 如果flag為真,表明賬戶中已有人存錢進去,則存錢方法阻塞

if(flag)

//①

{? ? ??

? ? ? ? ? wait();

? ? ? ? ? ? }else{

// 執行存款

System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? ? ? +" 存款:"+? depositAmount);

? ? ? ? ? ? ? ? balance += depositAmount;

? ? ? ? ? ? ? ? System.out.println("賬戶余額為:"+ balance);

// 將表示賬戶是否已有存款的旗標設為true

flag =true;

// 喚醒其他線程notifyAll();

? ? ? ? ? ? }

? ? ? ? }catch(InterruptedException ex)? ? ? ? {

? ? ? ? ? ex.printStackTrace();

? ? ? }

? ? }

// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法

public int hashCode(){

return accountNo.hashCode();

? ? }

public boolean equals(Object obj){

if(this== obj)returntrue;

if(obj !=null&& obj.getClass() == Account.class)? ? ? ? {

? ? ? ? ? ? Account target = (Account)obj;

return target.getAccountNo().equals(accountNo);

? ? ? ? }

return false;

? ? }

}

public class DrawThread extends Thread{

// 模擬用戶賬戶

private Account account;

// 當前取錢線程所希望取的錢數

private double drawAmount;

public DrawThread(String name , Account account? ? ? ? ,doubledrawAmount){

super(name);

this.account = account;this.drawAmount = drawAmount;

? ? }

// 重復100次執行取錢操作

public void run(){for(inti =0; i <100; i++ )? ? ? ? {? ? ? ? ? ? account.draw(drawAmount);

? ? ? ? }

? ? }

}

public class DepositThread extends Thread{

// 模擬用戶賬戶

private Account account;

// 當前取錢線程所希望存款的錢數

private double depositAmount;

public DepositThread(String name , Account account? ? ? ? ,doubledepositAmount){

super(name);

this.account = account;

this.depositAmount = depositAmount;

? ? }

// 重復100次執行存款操作

public void run(){

for(inti =0; i <100; i++ )? ? ? ? {

? ? ? ? ? ? account.deposit(depositAmount);? ? ? ? }

? ? }

}

public class DrawTest{

public static void main(String[] args){

// 創建一個賬戶

Account acct =newAccount("1234567",0);

newDrawThread("取錢者", acct ,800).start();

newDepositThread("存款者甲", acct ,800).start();

newDepositThread("存款者乙", acct ,800).start();

newDepositThread("存款者丙", acct ,800).start();

? ? }

}

上圖所示的是堵塞而不是死鎖,取錢者的線程已經執行結束,但是存錢者的線程只是在等待其他線程來取錢而已,并不是等待其他線程釋放同步監視器,不要把死鎖和程序堵塞等同起來.

使用Condition控制線程通信

如果程序使用Lock對象保證同步,則系統中不存在隱式地同步監視器,也就不能用wait(),notify(),notifyAll()方法進行線程通信了.

當使用Lock對象來保證同步時,Java提供了一個Condition類來保持協調,使用Conditon可以讓那些已經得到Lock對象卻無法繼續執行的線程釋放Lock對象,Conditon對象也可以喚醒其它處于等待的線程.

Conditon將同步監視器方法(wait(),notify(),notifyAll())分解成不同的對象,以便通過將這些對象和Lock對象組合使用,為每個對象提供多個等待集(wait-set).Lock替代了同步方法或同步代碼塊,Conditon替代了同步監視器的功能.

Conditon實例綁定在一個Lock對象上,要獲得特定Lock實例的Conditon實例,調用Lock對象的newConditon()方法即可.

Conditon類提供了如下三個方法:

await():類似于隱式同步器上的wait()方法,導致當前線程等待,直到其它線程調用該Conditon的signal()方法或signalAll()方法來喚醒線程.

signal():喚醒在此Lock對象上等待的單個線程.只有當前線程放棄對該Lock對象的鎖定后(使用await()方法),才可以執行被喚醒線程

signalAll():喚醒在此Lock對象上等待的所有線程.只有當前線程放棄對該Lock對象的鎖定后(使用await()方法),才可以執行被喚醒線程

下面程序通過Account使用Lock對象來控制同步,并使用Conditon對象來控制線程的協調運行.

public class Account{

// 顯式定義Lock對象

private finalLock lock =newReentrantLock();

// 獲得指定Lock對象對應的Condition

private final Condition cond? = lock.newCondition();

// 封裝賬戶編號、賬戶余額的兩個成員變量

private String accountNo;

private double balance;

// 標識賬戶中是否已有存款的旗標

private boolean flag =false;publicAccount(){}

// 構造器

public Account(String accountNo ,doublebalance){

this.accountNo = accountNo;this.balance = balance;

? ? }

// accountNo的setter和getter方法

public void setAccountNo(String accountNo){

this.accountNo = accountNo;

? ? }

public String getAccountNo(){returnthis.accountNo;? ? }

// 因此賬戶余額不允許隨便修改,所以只為balance提供getter方法,

public double getBalance(){returnthis.balance;? }

public void draw(doubledrawAmount){

// 加鎖

lock.lock();

try{

// 如果flag為假,表明賬戶中還沒有人存錢進去,取錢方法阻塞if(!flag)? ? ? ? ? ? {? ? ? ? ? ? ? ? cond.await();

? ? ? ? ? ? }else{

// 執行取錢

System.out.println(Thread.currentThread().getName()

? ? ? ? ? ? ? ? ? ? +" 取錢:"+? drawAmount);

? ? ? ? ? ? ? ? balance -= drawAmount;

? ? ? ? ? ? ? ? System.out.println("賬戶余額為:"+ balance);

// 將標識賬戶是否已有存款的旗標設為false。

flag =false;// 喚醒其他線程cond.signalAll();

? ? ? ? ? ? }

? ? ? }catch(InterruptedException ex)? ? ? ? {?

? ? ? ? ? ex.printStackTrace();

? ? ? ? }

// 使用finally塊來釋放鎖

finally{

? ? ? ? ? lock.unlock();

? ? ? ? }

? ? }

public void deposit(doubledepositAmount){

? ? ? ? lock.lock();

try{

// 如果flag為真,表明賬戶中已有人存錢進去,則存錢方法阻塞

if(flag)

// ①

{

//導致當前線程等待,知道其他線程調用該Conditon的signal()或signalAll()方法來喚醒該線程

cond.await();

? ? ? ? ? ? }else{

// 執行存款

System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? ? ? +" 存款:"+? depositAmount);

? ? ? ? ? ? ? balance += depositAmount;

? ? ? ? ? ? ? ? System.out.println("賬戶余額為:"+ balance);

// 將表示賬戶是否已有存款的旗標設為true

flag =true;

// 喚醒其他線程cond.signalAll();

? ? ? ? ? ? }

? ? ? ? }catch(InterruptedException ex)? ? ? ? {?

? ? ? ? ? ex.printStackTrace();

? ? ? }// 使用finally塊來釋放鎖

finally{??

? ? ? ? lock.unlock();?

? ? ? }?

? }

// 下面兩個方法根據accountNo來重寫hashCode()和equals()方法

public int hashCode(){returnaccountNo.hashCode();? ? }

public boolean equals(Object obj){

if(this== obj) return true;

if(obj !=null&& obj.getClass() == Account.class)? ? ? ? {?

? ? ? ? ? Account target =(Account)obj;

???????? return target.getAccountNo().equals(accountNo);

? ? ? }

returnfalse;

? ? }

}

這里只不過是現在顯式使用Lock對象來充當同步監視器,需要使用Condition對象來暫停,喚醒指定的線程.

使用堵塞隊列(BlockingQueue)控制線程通信

Java5提供了一個BlockingQueue接口,雖然BlockingQueue也是Queue的子接口,但它的主要作用不是作為容器,而是作為線程同步的工具.

BlockingQueue具有一個特征:當生產者線程試圖向BlockingQueue中放入元素時,如果該隊列已滿,則線程堵塞;當消費者線程試圖從BlockingQueue中取出元素時,如果該隊列已經已空,則該線程堵塞.

程序中兩個線程通過交替向BlockingQueue中放入元素取出元素,即可實現控制線程通信.

BlockingQueue提供下面兩個支持堵塞的方法:

put(E e):嘗試把e元素放入BlockingQueue中,如果該隊列的元素已滿,則堵塞線程

take():嘗試從BlockingQueue的頭部取出元素,如果該隊列的元素已空,則堵塞該線程.

BlockingQueue繼承了Queue接口,當然也可以使用Queue接口中的方法

在隊列尾部插入元素:add(E e),offer(E e)和put(E e)方法,當該隊列已滿時,這三個方法分別拋出異常,返回false,堵塞隊列.

在隊列頭部刪除并返回刪除的元素:remove(),poll(),和take()方法.當該隊列已空時,這三個方法分別會拋出異常,返回false,堵塞隊列.

在隊列頭部取出但不刪除元素:包括element()和peek()方法,當隊列已空時,這兩個方法分別拋出異常,返回false.

image.png

BlockingQueue包含如下5個實現類:

ArrayBlockingQueue:基于數組

LinkedBlockingQueue:基于鏈表

PriotityBlockingQueue:跟之前的PriotityQueue有點類似,在這里不做過多的介紹,自然順序

SynchronousQueue:同步隊列,該隊列的存取操作必須交替進行

DelayQueue:底層基于PriotityBlockingQueue實現,不過DelayQueue要求集合元素都實現Delay接口(該接口里有一個long getDelay()方法),DelayQueue根據集合元素的getDelay()方法的返回值進行排序.

下面使用ArrayBlockingQueue為例來介紹堵塞隊列的功能和用法.

importjava.util.concurrent.*;publicclassBlockingQueueTest{publicstaticvoidmain(String[] args)throwsException{// 定義一個長度為2的阻塞隊列BlockingQueue bq =newArrayBlockingQueue<>(2);? ? ? ? bq.put("Java");// 與bq.add("Java"、bq.offer("Java")相同bq.put("Java");// 與bq.add("Java"、bq.offer("Java")相同bq.put("Java");// ① 阻塞線程。//bq.add("Java");//拋出異常//bq.offer("Java");//返回false,元素不會被放入}}

與此類似的是:BlockingQueue已空的情況下:

使用take()方法取出元素會堵塞線程;

使用remove()方法嘗試取出元素將引發異常;

使用poll()方法取出元素將會返回false,元素不會被刪除.

下面程序利用BlockingQueue來實現線程通信

import java.util.concurrent.*;

class Producer extends Thread{

private BlockingQueue bq;

public Producer(BlockingQueue bq){

this.bq = bq;

? ? }

public void run(){

? ? ? ? String[] strArr =newString[]{"Java","Struts","Spring"};

????????? for(inti =0; i <999999999; i++ )? ? ? ? {?

? ? ? ? ? System.out.println(getName() +"生產者準備生產集合元素!");

? ? ? try{

? ? ? ? ? ? ? ? Thread.sleep(200);

??????? // 嘗試放入元素,如果隊列已滿,線程被阻塞bq.put(strArr[i %3]);

? ? ? ? ? }catch(Exception ex){ex.printStackTrace();}? ? ? ? ? ??????? System.out.println(getName() +"生產完成:"+ bq);

? ? ? }

? ? }

}

class Consumer extends Thread{

private BlockingQueue bq;

public Consumer(BlockingQueue bq){

this.bq = bq;

? ? }

public void run(){

while(true)? ? ? ? {

? ? ? ? ? System.out.println(getName() +"消費者準備消費集合元素!");try{? ? ? ? ? ? ? ? Thread.sleep(200);// 嘗試取出元素,如果隊列已空,線程被阻塞bq.take();? ? ? ? ? ? }catch(Exception ex){ex.printStackTrace();}? ? ? ? ? ? System.out.println(getName() +"消費完成:"+ bq);

? ? ? ? }

? ? }

}

public class BlockingQueueTest2{

public static void main(String[] args){

// 創建一個容量為1的

BlockingQueueBlockingQueue bq =newArrayBlockingQueue<>(1);// 啟動3條生產者線程

newProducer(bq).start();

newProducer(bq).start();

newProducer(bq).start();

// 啟動一條消費者線程

newConsumer(bq).start();

? ? }

}

本程序的BlockingQueue集合容量時1,因此3個生產者線程無法連續放入元素,必須等待消費者線程取出一個元素后,3個生產者線程的其中之一才能放入一個元素.

線程組合未處理異常

Java使用ThreadGroup來表示線程組,它可以對一批線程進行分類管理.Java允許程序直接對線程組進行控制.對線程組的控制相當于同時控制這批線程.

用戶創建的所有線程都屬于指定線程組,如果程序沒有顯示指定線程屬于哪個線程組,那么該線程屬于默認線程組.在默認情況下,子線程和創建它的父線程都處于同一線程組內,比如:A線程創建了B線程,并且沒有指定B線程屬于哪一個線程組,那么B線程屬于A線程所在的那個線程組.

一旦某個線程加入了指定的線程組之后,該線程將一直屬于該線程組,直到該線程死亡.線程運行中途不能改變它所屬的線程組.

Thread類提供了幾個構造器來設置新創建的線程屬于哪個線程組

Thread(ThreadGroup group, Runnable target):以target的run()方法作為線程執行體創建新線程,屬于group線程組.

Thread(ThreadGroup group, Runnable target,String name):以target的run()方法作為線程執行體創建新線程,該線程屬于group線程組,且線程名為name

Thread(ThreadGroup group, String name):創建新線程,新線程名字為name,屬于group線程組.

Thread類提供了一個getThreadGroup()方法來返回線程所屬的線程組,getThreadGroup()方法的返回值是ThreadGroup對象,表示一個線程組.

ThreadGroup類提供了下面兩個簡單的構造器來創建實例:

ThreadGroup(String name):以指定的線程組名字來創建新的線程組

ThreadGroup(ThreadGroup parent,String name):以指定的名字,指定的父線程組創建一個新線程組.

線程組總會有一個名字字符串類型的名字,該名字可以通過ThreadGroup的getName()方法來獲取,但是不允許改變線程組名字.

ThreadGroup類提供了如下幾個常用的方法來操作整個線程組里的所有線程:

int activeCount():返回此線程組中活動線程總數

interrupt():中斷此線程組中所有線程

isDaemon():判斷該線程組是否是后臺線程組

setDaemon(boolean daemon):把該線程組設置成后臺線程組.

setMaxPriority(int pri):設置線程組的最高優先級

class MyThread extends Thread{

// 提供指定線程名的構造器

publicMyThread(String name){super(name);? ? }

// 提供指定線程名、線程組的構造器

public MyThread(ThreadGroup group , String name){

????? super(group, name);

? ? }

public void run(){

for(inti =0; i <20; i++ )? ? ? ? {?

? ? ? ? ? System.out.println(getName() +" 線程的i變量"+ i);? ? ? ? }? ? }

}

public class ThreadGroupTest{

public static void main(String[] args){

// 獲取主線程所在的線程組,這是所有線程默認的線程組

ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();? ? ? ? System.out.println("主線程組的名字:"+ mainGroup.getName());

?System.out.println("主線程組是否是后臺線程組:"+ mainGroup.isDaemon());

newMyThread("主線程組的線程").start();

? ? ? ThreadGroup tg =newThreadGroup("新線程組");

? ? ? ? tg.setDaemon(true);

? ? ? System.out.println("tg線程組是否是后臺線程組:"+ tg.isDaemon());

? ? ? ? MyThread tt =newMyThread(tg ,"tg組的線程甲");

? ? ? tt.start();newMyThread(tg ,"tg組的線程乙").start();? ?

}

}

ThreadGroup內還定義了一個void uncaughtException(Thread t,Throwable e):該方法可以處理該線程組內的任意線程所拋出的未處理異常.該方法中的t代表出現異常的線程,e代表該線程拋出的異常.void uncaughtException(Thread t,Throwable e)該方法屬于Thread.UncaughtExceptionHandler接口里唯一的一個方法,該接口是Thread類的一個靜態內部接口

Thread類提供如下兩個方法來設置異常處理器:

static setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):為該線程類的所有線程實例設置默認的異常處理器.

setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):為指定的線程實例設置異常處理器

ThreadGroup類實現了Thread.UncaughtExceptionHandler接口,所以每個線程所屬的線程組都會作為默認的異常處理器.

當一個線程拋出未處理的異常時,JVM會首先查找該異常所對應的異常處理器(setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法設置的異常處理器),如果找到該異常處理器,則將調用該異常處理器處理該異常;否則JVM將會調用該線程所屬的線程組對象的uncaughtExceptio()方法來處理該異常.

線程組處理異常的默認流程如下:

1.如果該線程組有父線程組,則調用父線程組的uncaughtException()方法來處理該異常

2.如果該線程實例所屬的線程類有默認的異常處理器(由setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法設置的異常處理器),那么就調用該異常處理器來處理該異常.

3.如果該異常對象是ThreadDeath的對象,則不做任何處理;否則,將異常跟蹤棧的信息打印到System.err錯誤輸出流,并結束該線程

下面程序為主線程設置了異常處理器,當主線程運行拋出未處理的異常時,該異常處理器會起作用.

class MyExHandler implements Thread.UncaughtExceptionHandler{

// 實現uncaughtException方法,該方法將處理線程的未處理異常

public void uncaughtException(Thread t, Throwable e){

? ? ? System.out.println(t +" 線程出現了異常:"+ e);??

}}publicclassExHandler{publicstaticvoidmain(String[] args){

// 設置主線程的異常處理器

Thread.currentThread().setUncaughtExceptionHandler? (newMyExHandler());

inta =5/0;// ①System.out.println("程序正常結束!");

? }

}

結果為:

Thread[main,5,main] 線程出現了異常:java.lang.ArithmeticException: / by zero

說明異常處理器與通過catch捕捉異常是不同的,當使用catch捕捉異常時,通常不會向上傳播給上一級調用者;但使用異常處理器對異常進行處理之后,異常依然會傳播給上一級調用者.

線程池

系統啟動一個新線程的成本是比價高的,因為涉及到與操作系統交互,在這種情況下,使用線程池可以很好的提高性能,尤其是當程序中需要創建大量生存期很短的線程時,更應該考慮使用線程池

線程池在系統啟動時即創建大量空閑的線程,程序將一個Runnable對象或Callable對象傳給線程池,線程池就會啟動一個線程來執行它們的run()或call()方法,當run()或call()方法執行結束后,該線程并不會死亡,而是再次返回線程池中稱為空閑狀態,等待執行下一個Runnable對象的run()或call()方法.

使用線程池可以有效控制系統中并發線程的數量,當系統中包含大量并發線程時,會導致系統性能劇烈下降,甚至JVM崩潰,而線程池的最大線程數參數可以控制系統中并發線程數不超過此數.

Java8改進的線程池

Java5新增了一個Executors工廠類來產生線程池,該工廠類提供了如下幾個靜態工廠方法來創建線程池

使用線程池來執行線程任務的步驟如下:

1.調用Executor類的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池.

2.創建Runnable實現類或Callable實現類的實例,作為線程執行任務.

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

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

下面程序使用線程池來執行指定Runnable對象所代表的任務

import java.util.concurrent.*;

public class ThreadPoolTest{

publicstaticvoidmain(String[] args)throwsException{

// 創建足夠的線程來支持4個CPU并行的線程池

// 創建一個具有固定線程數(6)的線程池

ExecutorService pool = Executors.newFixedThreadPool(6);

// 使用Lambda表達式創建Runnable對象

Runnable target = () -> {for(inti =0; i <100; i++ )? ? ? ? ? ? {? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? ? ? +"的i值為:"+ i);

? ? ? ? ? }

? ? ? };

// 向線程池中提交兩個線程pool.submit(target);

? ? ? ? pool.submit(target);// 關閉線程池pool.shutdown();

? ? }

}

Java8增強的ForkJoinPool

Java7提供了ForkJoinPool來支持將一個任務分解為多個"小任務"并行計算,再把多個"小任務"的結果合并成總的計算結果.ForkJoinPool是ExecutorService的實現類,因此是一種特殊的線程池.

ForkJoinPool提供如下兩個常用的構造器:

ForkJoinPool(int parallelism):創建一個包含parallelism個并行線程的ForkJoinPool.

ForkJoinPool():以Runtime.availableProcessors()方法的返回值作為parallelism參數來創建ForkJoinPool

Java8為ForkJoinPool增加了通用池功能.ForkJoinPool類通過如下兩個靜態方法提供通用池功能:

ForkJoinPool commonPool():該方法返回一個通用池,通用池的運行狀態不會受shutdown()或shutdownNow()方法的影響.如果程序直接執行System.exit(0)來終止虛擬機,通用池以及通用池中正在執行的任務都會被自動終止.

int getCommonPoolParallelism():該方法返回通用池的并行級別.

創建了ForkJoinPool實例之后,就可以調用ForkJoinPool的submit(ForkJoinTask task)或invoke(ForkJoinTask task)方法來執行指定的任務了.其中ForkJoinTask代表一個可以并行,合并的任務.ForkJoinTask是一個抽象類,它有兩個抽象子類:RecursiveAction和RecursiveTask.其中RecursiveAction代表有返回值的任務,RecursiveTask代表沒有返回值的任務.

image.png

執行沒有返回值的大任務為例,下面程序示例將一個大任務拆分成多個小任務,并將任務交給ForkJoinPool來執行

importjava.util.concurrent.*;// 繼承RecursiveAction來實現"可分解"的任務

class PrintTask extends RecursiveAction{

// 每個“小任務”只最多只打印50個數

private static final int THRESHOLD =50;

private int start;

private int end;

// 打印從start到end的任務

public PrintTask(intstart,intend){

this.start = start;this.end = end;

? ? }

@Override

protected void compute(){

// 當end與start之間的差小于THRESHOLD時,開始打印if(end - start < THRESHOLD)? ? ? ? {

for(inti = start ; i < end ; i++ )? ? ? ? ? ? {

? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName()? ? ? ? ? ? ? ? ? ? +"的i值:"+ i);

? ? ? ? ? }

? ? ? ? }else{

// 如果當end與start之間的差大于THRESHOLD時,即要打印的數超過50個

// 將大任務分解成兩個小任務。

int middle = (start + end) /2;

? ? ? ? ? PrintTask left =newPrintTask(start, middle);

? ? ? ? ? ? PrintTask right =newPrintTask(middle, end);

// 并行執行兩個“小任務”left.fork();

? ? ? ? ? right.fork();

? ? ? }

? }

}

public class ForkJoinPoolTest{

public static void main(String[] args)throwsException{

? ? ? ForkJoinPool pool =newForkJoinPool();

// 提交可分解的PrintTask任務

pool.submit(newPrintTask(0,300));

? ? ? ? pool.awaitTermination(2, TimeUnit.SECONDS);

// 關閉線程池pool.shutdown();? ?

}

}

如果大任務是有返回值的任務,則可以讓任務繼承RecursiveTask,其中泛型T代表該任務的返回類型.

import java.util.concurrent.*;

import java.util.*;

// 繼承RecursiveTask來實現"可分解"的任務

class CalTask extends RecursiveTask{

// 每個“小任務”只最多只累加20個數

private static final int THRESHOLD =20;

private int arr[];

private int start;

private int end;

// 累加從start到end的數組元素

public CalTask(int[] arr ,intstart,intend){

this.arr = arr;this.start = start;this.end = end;

? ? }

@OverrideprotectedIntegercompute(){

int sum =0;

// 當end與start之間的差小于THRESHOLD時,開始進行實際累加

if(end - start < THRESHOLD)? ? ? ? {

for(inti = start ; i < end ; i++ )? ? ? ? ? ? {

? ? ? ? ? ? ? sum += arr[i];? ? ? ? ? ? }

return sum;

? ? ? }else{

// 如果當end與start之間的差大于THRESHOLD時,即要累加的數超過20個時

// 將大任務分解成兩個小任務。

intmiddle = (start + end) /2;??

? ? ? ? CalTask left =newCalTask(arr , start, middle);?

? ? ? ? ? CalTask right =newCalTask(arr , middle, end);

// 并行執行兩個“小任務”

left.fork();

? ? ? ? ? ? right.fork();

// 把兩個“小任務”累加的結果合并起來

return left.join() + right.join();

// ①

}

? }

}

public class Sum{

public static void main(String[] args)throwsException{

int[] arr =newint[100];

? ? ? ? Random rand =newRandom();

inttotal =0;

// 初始化100個數字元素

for(inti =0, len = arr.length; i < len ; i++ )? ? ? ? {

int tmp = rand.nextInt(20);

// 對數組元素賦值,并將數組元素的值添加到sum總和中。

total += (arr[i] = tmp);

? ? ? ? }

? ? ? System.out.println(total);

// 創建一個通用池

ForkJoinPool pool = ForkJoinPool.commonPool();

// 提交可分解的CalTask任務

Future future = pool.submit(newCalTask(arr ,0, arr.length));? ? ? ? System.out.println(future.get());

// 關閉線程池

pool.shutdown();? ?

}

}

線程相關類

ThreadLocal類

ThreadLocal它代表一個線程局部變量,通過把數據放在ThreadLocal中就可以讓每個線程創建一個該變量的副本,從而避免并發訪問的線程安全問題.

ThreadLocal類支持泛型支持.通過使用ThreadLocal類可以簡化多線程編程中的并發訪問,使用這個工具類可以簡捷地隔離多線程程序的競爭資源.

線程局部變量(ThreadLocal)的功用非常簡單,就是為每一個使用該變量的線程都提供一個變量值的副本,使每一個使用該變量的線程都提供一個變量值的副本,使每一個線程都可以獨立地改變自己的副本,而不會和其他線程的副本沖突.

ThreadLocal類的用法非常簡單,它只提供如下三個public方法.

T get():返回此線程局部變量中當前線程副本中的值

void remove():刪除次線程局部變量中當前線程的值

void set(T value):設置此線程局部變量中當前線程副本中的值

classAccount{/* 定義一個ThreadLocal類型的變量,該變量將是一個線程局部變量

? ? 每個線程都會保留該變量的一個副本 */

private ThreadLocal name = newThreadLocal<>();

// 定義一個初始化name成員變量的構造器

public Account(String str){

this.name.set(str);

// 下面代碼用于訪問當前線程的name副本的值

System.out.println("---"+this.name.get());

? ? }

// name的setter和getter方法

public String getName(){

returnname.get();

? ? }

public void setName(String str){

this.name.set(str);

? ? }

}

class MyTest extends Thread{

// 定義一個Account類型的成員變量

private Account account

;publicMyTest(Account account, String name){

super(name);

this.account = account;

? }publicvoidrun(){

// 循環10次for(inti =0; i <10; i++)? ? ? ? {

// 當i == 6時輸出將賬戶名替換成當前線程名if(i ==6)? ? ? ? ? ? {? ? ? ? ? ? ? ? account.setName(getName());

? ? ? ? ? ? }

// 輸出同一個賬戶的賬戶名和循環變量

System.out.println(account.getName()? ? ? ? ? ? ? ? +" 賬戶的i值:"+ i);

? ? ? ? }

? }

}

public class ThreadLocalTest{

public static void main(String[] args){

// 啟動兩條線程,兩條線程共享同一個AccountAccount at =newAccount("初始名");

/*

? ? ? ? 雖然兩條線程共享同一個賬戶,即只有一個賬戶名

? ? ? ? 但由于賬戶名是ThreadLocal類型的,所以每條線程

? ? ? ? 都完全擁有各自的賬戶名副本,所以從i == 6之后,將看到兩條

? ? ? ? 線程訪問同一個賬戶時看到不同的賬戶名。

? ? ? ? */

newMyTest(at ,"線程甲").start();newMyTest(at ,"線程乙").start ();

? ? }

}

ThreadLocal和其他同步機制一樣,都是為了解決多線程找那個對同一變量的訪問沖突,在普通的同步機制中,是通過對象加鎖來實現多個線程對同一個變量的安全訪問的,該變量是多個線程共享的,所以要使用這種同步機制,要很細致的分析在什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放該對象的鎖等等,在這種情況下系統并沒有將這份資源復制多份,只是采用安全機制來控制對這份資源的訪問而已.

ThreadLocal從另一個角度來解決多線程的并發訪問,ThreadLocal將需要并發訪問的資源復制多份,每個線程喲擁有一份資源,每個線程擁有自己的資源副本,從而也就沒有必要對該變量進行同步了.ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的整個變量封裝進ThreadLocal,或者把該對象與線程相關的狀態使用ThreadLocal保存.

ThreadLocal并不能代替同步機制,同步機制是為了同步多個線程對相同資源的并發訪問,是多個線程之間進行通信的有效方式,而ThreadLocal是為了隔離多個線程的數據共享,從根本上上避免多個線程之間對共享資源(變量)的競爭,也就不需要對多個線程進行同步了.

如果多個線程之間需要共享資源,達到線程通信的功能,那么就使用同步機制;如果僅僅需要隔離多個線程之間的共享沖突,可以使用ThreadLocal

包裝線程不安全的集合

前面提到了ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap都是線程不安全的集合,當多個并發線程訪問這些集合存取元素時,就可能會破壞這些集合的數據完整性.

可以使用Collections提供的類方法把這些集合編程線性安全的集合.

如果需要把某個集合包裝成線性安全的集合,應該在創建之后立即包裝

//使用Collections的synchronizedMap方法將一個普通的HashMap包裝成一個線程安全的類HashMap m=Collections.synchronizedMap(newHashMap());

線程安全的集合類

從Java5開始,在java.util.concurrent包下提供了大量支持高效并發訪問的集合接口和實現類

作者:小徐andorid

鏈接:http://www.lxweimin.com/p/87a5f9e41238

來源:簡書

簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 進程和線程 進程 所有運行中的任務通常對應一個進程,當一個程序進入內存運行時,即變成一個進程.進程是處于運行過程中...
    小徐andorid閱讀 2,844評論 3 53
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,494評論 1 15
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,987評論 1 18
  • 杭州街頭,普天蓋地都是京東的廣告,廣告的中心概念是“您以為的是只是你以為的”。概念支撐點是,偏遠山區可以送...
    豬只好愛豬閱讀 234評論 0 0
  • 今天在工作坊里有一個演出,演繹一個現場發生的故事。要演繹一個對于在現場很重要,讓我感覺不能隨意猜測的人物,我的大腦...
    春暉一人一故事劇場閱讀 711評論 0 3