Java學習之多線程

前言:日常編程工作當中經常聽到“線程”,“線程池”,“多線程”的概念,以前對這些總是傻傻分不清楚,閒暇之餘將這些知識點進行一個系統的學習和總結。在介紹線程之前,首先必須瞭解“進程”的概念,下面就“進程”、“線程”、“多線程”等等關於線程的知識做一個歸納,方便以後查閱。

一、進程;

       進程是一個正在進行中的程序,每一個進程都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元。或者說,一個進程就是一個應用程序,都有獨立的內存空間。打個比方,Windows 打開任意一個應用軟件就意味着windows打開了一個進程,通常在Java的開發環境下啓動JVM 就相當於開啓了java進程,在一個操作系統當中可以同時啓動多個進程。

二、線程

      線程是指進程當中的一個執行場景(即執行流程),也是進程中的一個獨立控制單元,控制則進程的執行。一個進程至少有一個線程。就像啓動JVM的時候就會有一個java.exe的應用程序被啓動。該進程當中有一個線程負責java程序的運行,這個線程存在於java的入口mian()函數當中,也被稱爲java的主線程,與此同時運行的還有其他線程,比如垃圾回收機制的線程。

三、多線程

       首先來描述一個場景,一臺計算機同時開啓了兩個進程,一個遊戲,一個音樂。此時對於單核計算機來講,在某一個時間節點時,兩個進程並非同時進行的,因爲CPU此刻只能做一件事情,我們之所以能感到兩個程序在同時運行,其實就是計算機在這兩個進程之間高頻率的切換。多線程的存在其實就是爲了提高CPU的使用率。

       線程和線程共享“堆內存和方法區內存”,棧內存是獨立的,一個線程一個棧。

       引申介紹;java命令會啓動JVM ,此時就相當於啓動了一個應用程序(或者說是進程)。該程序就會啓動一個主線程,然後主線程會調用某個類的main方法。可見main 函數運行在主線程當中,此前所有的線程都是單線程。

四、線程的生命週期及五種基本狀態;

                      

                                                                 圖  4-1 

             基本狀態;

        1、新建狀態(new):當線程對象創建後,即進入了新建狀態;如  Thread  t  = new Thread();

        2、就緒狀態(Runnable):當調用線程的start()方法(t.start();),線程即進入就緒狀態,處於就緒狀態的線程,只能說此線程已經做好了準備,隨時等待CPU調度執行,並不是t.start();之後線程就會立即執行,

        3、運行狀態(Running):當CPU開始調度處於就緒狀態的線程的時候,此線程在能正式執行,此時正式進入運行狀態。

        4、阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權從而停止執行,進入阻塞狀態,知道其進入到就緒狀態,纔有機會被CPU再次調用然後進入到運行狀態,根據阻塞原因的不同,阻塞又分爲三種:

              4.1、等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入阻塞狀態。 

              4.2、同步阻塞:線程在獲取Synchronized同步鎖失敗(因爲鎖被其他線程所佔用),會進入到同步阻塞狀態。

              4.3、其他阻塞:通過調用現成的sleep()或join()或發出了I/O的請求時,線程會進入阻塞狀態,當sleep()執行超時,join()等待線程終止或者超時,或者I/O處理完成時,線程重新轉入就緒狀態。

        5、死亡狀態(Dead):線程執行完了或者因異常,退出了run()函數,該線程結束生命週期。

五、java多線程的創建及啓動;

       常見的創建多線程有三種方法;

       1、繼承Thread類,重寫run方法。

public class MyThreadTest extends Thread {
	private int i=0;
	@Override
	public void run(){
		for(i=0;i<100;i++){
			System.out.println(Thread.currentThread().getName() + "-自創線程- " + i);
		}
	}
}


public class NotInitialization {
	public static void main(String[] args) {
	    for(int i=0;i<100;i++){
	    	System.out.println(Thread.currentThread().getName() + " -主線程- " + i);
	    	if(i==30){
	    		Thread  t1=  new MyThreadTest();
	    		t1.run();
	    	} else  if(i==60){
	    		Thread  t2=  new MyThreadTest();
	    		t2.run();
	    	}
	    }
	}
}

   如上所示:繼承Thread類通過重寫run()方法定義了一個新的線程類MyThreadTest,其中run()方法體代表了線程需要完成任務,稱之爲線程執行體,當創建此線程類對象時,一個新的線程得以創建,並進入到線程新建狀態。通過調用線程對象引用的start()方法,使得線程進入到就緒狀態,此時這個線程並一定馬上執行,執行的時機取決於CPU的條用時機。

        2、實現Runnable接口,並重寫接口的run(),該run()方法同樣作爲線程的執行主體,創建Runnable實現類的實例,並以此實例作爲Thread類的target來創建Thread對象,該Thread對象纔是真正的線程對象。

public class MyThreadTest implements Runnable {
	
	private int i=0;
	@Override
	public void run(){
		for(i=0;i<100;i++){
			System.out.println(Thread.currentThread().getName() + "-自創線程- " + i);
		}
	}
}

public class NotInitialization {
	public static void main(String[] args) {
	    for(int i=0;i<100;i++){
	    	System.out.println(Thread.currentThread().getName() + " -主線程- " + i);
	    	Runnable r= new MyThreadTest();
	    	if(i==30){
	    		Thread  t1=  new Thread(r);
	    		t1.run();
	    	} else  if(i==60){
	    		Thread  t2=  new Thread(r);
	    		t2.run();
	    	}
	    }
	}
}

              通過上面的創建線程結合Thread底層發現,Thread類實現了Runnable接口,如下圖;

                 

              問題:此時程序中的Run()方法,到底是屬於Thread中的還是重寫的Runnable之中的,請自行查看底層實現。

         3、使用Callable和 Futrue接口創建線程,具體是創建Callable接口的實現類,並使用call()方法,並使用FutrueTask類來包裝Runnable類的對象,且以此FutrueTask對象作爲Thread對象的target來創建線程。

              此方法不做深入研究。

        4、那麼創建線程通過繼承Thread類和實現Runnable接口有何區別。

              4.1、單繼承具有侷限性,比如某個類應該繼承過其他父類。

              4.2、繼承Thread類線程代碼存放在Thread子類中run()方法中。實現Runnaable接口線程代碼則存放在接口子類的方法中。

         5、Thread類的start() 與 run()

               5.1、start():先看API給的解釋;

                        時線程開始執行,Java虛擬機開始調用線程的run()方法。結果是兩個線程併發的執行  。如下;

                        

                        當前線程(執行的start()函數)和另一個線程(執行run())多次啓動一個線程是非法的。特別是當線程已經結束執行後,不能再重新啓動。

                        使用start()方法來啓動線程,真正意義上的實現了多線程,這是無需使用run()方法體執行。通過Thread類的start()方法,來啓動的線程處於就緒(可運行)但未運行狀態,一旦得到CPU調度就開始執行run()方法(即線程主體),它包含了要執行線程的內容,run方法結束,則此線程終止。

               5.2、run():如果該線程是使用獨立的Runnable對象構造的,則調用runnable對象的Run方法,否則該方法不執行操作並返回。Thread的子類應該重寫該方法。run()方法只是類的一個普通方法而已,如果直接調用Run方法,程序中依然只有主線程這一個線程,其程序執行路徑還是隻有一條,還是要順序執行,還是要等待run方法體執行完畢後纔可繼續執行下面的代碼,這樣就沒有達到寫線程的目的。

               5.3、總結:調用start方法方可啓動線程,而run方法只是thread的一個普通方法調用,還是在主線程裏執行。

六、Java多線程的就緒,運行和死亡狀態

        就緒狀態轉換爲運行狀態:當此程序得到處理器資源;

        運行狀態轉換爲就緒狀態:當此線程主動調用yield()方法或在運行過程中失去處理器資源。

        就緒狀態轉換爲死亡狀態:當此線程執行體執行完畢或者發生異常。

        理解此處的狀態換換描述參考四、線程的生命週期及五種基本狀態中圖 4-1。

        注:當線程調用yield()函數時,線程從運行狀態轉換爲就緒狀態,但接下來CPU調度就緒狀態中的哪個線程具有一定的隨機性,因此,可能會出現A線程調用了yield()方法後,接下來CPU仍然調度了A線程的情況。(瞭解即可)

七、多線程的安全問題

       7.1、多線程出現安全問題的原因: 當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了部分,剩下的部分由其他線程參與進來,此時有可能導致數據共享出現錯誤。

       7.2、解決思路:對多條操作共享數據的語句,只能讓一個線程都執行完,否則其他線程不能參與執行。

       7.3、解決辦法:java對於多線程的安全問題提供了專業的解決辦法,就是同步代碼塊。Synchronized(對象){需要被同步的代碼},對象如同鎖,持有鎖的線程可以在同步中執行,沒有持有鎖的線程即使獲取cpu執行權,也進不去,因爲沒有獲取鎖。使用同步鎖相應的增加了一個判斷同步鎖的線程,耗費資源。

       7.4、同步鎖的使用前提:1、必須有兩個或者兩個以上的線程。2、這些線程必須使用同一個同步鎖。(定義同步函數(即同步鎖)只需要在方法用synchronized修飾即可   這裏不再贅述)注:多線程死鎖。同步中嵌套同步會出現死鎖。

       7.5、多線程的單例模式(懶漢模式):懶漢式與餓漢式的區別:懶漢式能延遲實例的加載,如果多線程訪問時,懶漢式會出現安全問題,可以使用同步來解決,用同步函數和同步代碼都可以,但是比較低效,用雙重判斷的形式能解決低效的問題,加同步的時候使用的鎖是該類鎖屬的字節碼文件對象。

八、Java線程池;

        線程池簡介:所謂線程池就是將多個線程放在池子裏(池化技術),然後在需要線程的時候不再是創建線程,而是從線程池裏變直接取一個可用的線程執行任務。線程池的關鍵在於其爲我們管理了多個線程,無需我們去一一創建線程,只需要關係核心業務,然後去線程池裏直獲取線程,在任務執行完成後線程不會被銷燬,而是會被重新放回到線程池當中,等待機會再去執行任務。

        線程池的作用:首先是線程池爲們提高了一種簡易的多線程編程方案,無需認爲去管理這些線程。其次,在使用線程的時候創建和銷燬線程的代價是很高昂的,甚至我們創建和銷燬線程所消耗的資源要遠大於程序本身運行所需的資源,在線程池接管了這一部分業務之後,這種矛盾就得到了緩解。最後,一個程序當中的線程過多的話,可能會出現由於線程之間的頻繁切換,造成的安全等問題。學完理論,要看實踐

   下面介紹創建四種線程池的方法:

    1、newCachedThreadPool:可緩存線程池,如果線程池超過處理需要,可靈活回收空閒線程,如果無空閒線程,則創建。線程池爲無限大,當第二個任務執行時第一個任務已經完成,則會複用這個線程,不會重複創建。

    2、newFixedThreadPool :定長線程池,可控制線程最大併發數,超出線程則會在隊列等待。

    3、ScheduledThreadPool:定數線程池,支持定時和週期性的執行任務。

    4、newSingleThreadExecutor:單個線程線程池,使用唯一的工作線程來執行任務,保證所有的任務按照指定的順序執行。

 Java中線程池的頂級接口是Executor,但其實Executor並不是一個線程池,只是也執行線程的工具而已,真正的線程池接口是ExecutorService ,下面介紹幾個線程池重要的類;

       ExceutorService:真正的線程池接口;

       ScheduledExecutorService:能和Timer/TimerTask類似,解決那些需要任務重複執行的問題。

       ThreadPoolExecutor:ExceutorService的默認實現。

       ScheduledThreadPoolExecutor:繼承自ThreadPoolExecutor的ScheduledExecutorService接口實現,週期性任務調度的類實現。

       

 

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