Java線程建立形式 Thread構造詳解 多線程中篇(五)

Thread做爲線程的抽象,Thread的實例用於描述線程,對線程的操縱,就是對Thread實例對象的管理與控制。
建立一個線程這個問題,也就轉換爲如何構造一個正確的Thread對象。

構造方法列表

image_5c5bc7ed_3345
image_5c5bc7ed_4d7c

構造方法核心

如前面兩個圖所示,Thread共有8個構造方法
並且全部的構造方法都依賴於init方法
    private void init(ThreadGroup g, Runnable target, String name,long stackSize)
因此換一個角度思考,能夠認爲只有一個構造方法
image_5c5bc7ed_3ff0
這「惟一的一個構造方法」調用的是五個參數的init方法
image_5c5bc7ed_127a
因此說,儘管有8個構造方法,可是內部底層調用的都是init方法
這是一種編碼規範與設計思惟---「構造方法中不設置初始化邏輯,若是須要,那麼請將初始化邏輯進行封裝」
對於Thread類來講,五個參數的init方法,就是這個初始化邏輯的封裝方法
全部的構造方法都依賴他。

最大集合

從init的參數方法簽名來看,構造一個Thread最多須要五個值,也就是說對於一個基本的Thread,可以運行的Thread,最大集合爲五個;
可是經過構造方法能夠看得出來,所有都是調用的四個版本的init方法,都沒有傳遞AccessControlContext acc,在五個參數的版本中有設置默認值。
因此目前(1.8)支持Thread運行的構造參數最大集合個數爲四,他們分別是:
  • ThreadGroup g
  • Runnable target
  • String name
  • long stackSize

ThreadGroup g

ThreadGroup表示該線程所在的線程組,若是沒有顯式指定,那麼底層調用init時,傳遞的參數爲null
若是參數傳遞爲null的話,ThreadGroup會有默認值的設置
若是有安全管理器,會請求管理器進行設置,若是安全管理器不存在或者根本就沒有明確的指示,那麼將會獲取父線程的所在的線程組
父線程就是建立他的線程
        Thread parent = currentThread();
image_5c5bc7ed_15a1
因此,ThreadGroup是非必填項,若是不進行設置,會有默認初始值

Runnable target

Runnable用於封裝線程任務
Runnable 是一個接口,只有一個run方法,任務的具體內容封裝在run方法中
這是一個抽象方法,另外注意到在1.8中,他成爲了一個函數式接口,也就是說可使用Lambda表達式直接寫Runnable
image_5c5bc7ed_728a
另外還須要注意到,Thread實現了Runnable接口,實現了run方法
image_5c5bc7ed_64a5
也就是說,Thread天生自帶一個任務,這個任務是什麼?
他的任務就是若是target不爲null,那麼執行target.run(); 方法
而這個target就是經過構造方法注入進來,構造方法對內部變量target進行設置
image_5c5bc7ee_4  
當線程建立以後,經過start方法進入就緒狀態,等待處理機的調度,一旦得到運行,線程將會執行Thread的run方法。
注意到,是Thread中的run方法,而這個run方法是調用的target.run();
因此,很顯然,想要設置任務,要麼繼承Thread,重寫run方法,此時你的任務邏輯覆蓋了Thread中的邏輯,執行的是你指望的代碼;
要麼就是設置target,不過沒有setter,只可以經過構造方法注入;
總結下:
若是不對Thread的任務進行設置,由於Thread自身就是一個Runnable,自己具有任務,只不過不設置target的話至關因而一個空方法,沒什麼意思,你新起一個線程,結果什麼都不作,嘛意思嘛;
若是想要設置任務,重點是run方法,對run方法的設置能夠經過繼承Thread而後覆蓋,要麼就是經過構造方法設置Runnable target,只有這兩種方式。

String name

每一個線程,都有本身的名稱,若是不進行設置,那麼將會有一個默認的名字,以字符串「Thread-」開頭,而後會有一個遞增的序列變化
image_5c5bc7ee_7447
因此,對於線程名稱,若是不設置對程序的正確性、效率等都不會有任何問題

long stackSize

每一個線程都有私有的虛擬機棧,經過這個值能夠設置棧空間的大小,內部有屬性stackSize,設置的就是這個值
堆棧大小是虛擬機要爲該線程堆棧分配的地址空間的近似字節數
在某些平臺上,指定一個較高的 stackSize 參數值可能使線程在拋出 StackOverflowError 以前達到較大的遞歸深度
若是指定一個較低的值將容許較多的線程併發地存在,且不會拋出 OutOfMemoryError(或其餘內部錯誤)
stackSize 參數(若是有)的做用具備高度的平臺依賴性,某些平臺這個值均可能被忽略
若是這個值設置爲0表示忽略設置
image_5c5bc7ee_320e
因此,對於stackSize能夠進行設置,若是不設置默認是0,表示忽略該參數的設置。

最小集合

綜上所述,ThreadGroup g會有一個默認值經過安全管理器得到或者同父線程;String name能夠設置,不設置也會自動生成一個默認值;
long stackSize依賴平臺嚴重,不建議設置,默認指定爲0,表示忽略參數的設置;
對於Runnable target,若是不進行設置,也會存在一個默認的run方法,可是至關於空方法,毫無價值,因此你必須想辦法將任務進行設置。
因此,一個線程運行初始化設置的最小集合爲「任務封裝」,究竟是覆蓋run方法仍是傳入Runnable對象?您看狀況來
 
多線程的存在就是爲了執行任務的,因此,若是想讓一個線程有意義,Runnable target是必須存在的
Runnable target 是一個線程存在的必要條件,不然沒有意義,因此必須設置(儘管你不設計對運行上來講不會出錯)

start方法與run方法

咱們已經很明確的說明,run方法來自於Runnable接口,用於封裝須要執行的任務。
Thread是一個類,繼承了Runnable接口,Thread類能夠被實例化,Thread實現了run方法,因此Thread有一個run方法
因此,你看,run方法就是一個普通的方法
單純的看待run方法,Thread就是一個普通的類,Runnable就是一個普通的接口,他有一個抽象方法run,Thread實現了他
若是運行run方法,就跟平時調用一個對象的方法沒什麼區別,因此run方法的調用跟多線程沒有半毛錢關係,你就是調用一個對象的一個方法而已
全部的一切,都仍是在主線程中執行,沒有產生任何新的線程。
 
對於start方法,代碼以下
image_5c5bc7ee_1ff4
start方法的調用,將會使使該線程開始執行,Java 虛擬機將會調用該線程的 run 方法,接着就是線程併發的運行了
能夠看得出來,start方法並無調用run方法,關鍵是在於start0,這是一個native方法,依賴於本地方法,畢竟你JVM再牛逼仍是要調用底層,有本事你本身跑起來一個看看?
對run方法的執行也是在start0中觸發的,若是start0正確執行,沒有拋出異常,將會設置標誌位started=true;
 
另外,一個線程若是一旦啓動,再次啓動時會拋出異常
看獲得threadStatus用於標誌是否尚未啓動,若是不等於0,說明已經啓動了。
image_5c5bc7ee_3bbb
因此說,說到這裏,start和run方法有什麼區別?
他們就沒什麼類似的地方,start用於線程的初始化,而且調度執行run方法封裝的任務,run卻僅僅是封裝了任務代碼的一個普通方法。
一個封裝了線程的初始化邏輯,一個只是單純的任務封裝。
對於Thread中start方法和run方法的設計理念,是否是模板方法模式的應用?模板方法模式的意圖以下:
定義一個操做中的算法的骨架,而將一些步驟延時到子類中
TemplateMethod使得子類能夠不改變一個算法的結構便可從新定義算法的某些特定步驟
全部的線程初始化的邏輯是相同的,可是每一個線程須要執行的任務是千差萬別的;
在Thread中,start方法構建了初始化的邏輯,而將具體的行爲轉移到run方法中。

建立線程

隨便百度一下「java建立線程方式」會出來一大堆文章,有說三種方式,也有的說四種方式(線程池也算一種?)
image_5c5bc7ee_666c
本人不能說人家的就是錯的,可是至少是不許確的。
前面已經提到過,Thread是Java語言自己對線程的抽象,也就是說在Java中,線程只有一種形式,那就是Thread的實例形式存在。
如何建立一個Thread的實例對象?
只有一個途徑,那就是藉助於new
image_5c5bc7ee_20fb
對於線程任務的執行,new Thread以後,調用start方法,會調用Thread的run方法,Thread自帶一個run方法,實現自Runnable接口
image_5c5bc7ee_635f
因此,對於線程任務的設置,換一個問題就是:「如何改變這個run方法爲你須要的任務代碼?」
因此,你能夠繼承Thread,重寫run方法,徹底替代掉這個方法
Thread的子類仍舊是Thread,經過重寫run方法後將線程任務進行封裝,也就是有了任務代碼,new Thread子類().start()便可。
另外,這個方法很顯然,並無作什麼,只是透傳到target的run方法,因此若是對target進行設置,也能夠達到效果
因此準確的說,設置run方法,封裝任務代碼的途徑有兩種
  • 繼承Thread,重寫run方法;
  • 經過構造方法傳遞Runnable;
image_5c5bc7ee_2030
第一種途徑--繼承Thread,重寫run方法
image_5c5bc7ee_f35
第二種途徑--使用Runnable實例對象構造
image_5c5bc7ee_7d70
對於網上的有些示例,好比下面所示,從第一行「MyThread implements Runnable」就會給人誤導,他明明是一個Runnable,你起個名字MyThread?????線程???
反正多年前剛剛接觸時我還覺得Thread是Runnable(從實現上來講Thread是Runnable類型,可是實現接口是爲了run方法,邏輯上來講,線程是線程,線程任務是線程任務)
建立一個線程,跟Runnable沒半毛錢關係,任務封裝纔跟Runnable息息相關。
public class MyThread implements Runnable {//實現Runnable接口
  public void run(){
  //重寫run方法
  }
}
 
public class Main {
 
  public static void main(String[] args){
    //建立並啓動線程
    MyThread myThread=new MyThread();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者 new Thread(new MyThread2()).start();
  }
}
還有一種形式是實現Callable接口,而且藉助於FutureTask
image_5c5bc7ee_3dce
其實這根本上也是一種使用Runnable實例構造的另一種形式,咱們分析下這個過程
Callable是一個接口(1.8後函數式接口),包含一個call方法,裏面能夠用來封裝線程執行任務,不一樣於run方法,能夠用有返回值(具體細節後續會詳細說明)
image_5c5bc7ee_64b3
而FutureTask是個什麼東西?看下圖,很顯然,他就是一個Runnable
image_5c5bc7ee_5c79
內部持有一個Callable,能夠經過構造方法進行注入
    FutureTask futureTask = new FutureTask(myCallable);
image_5c5bc7ee_277
FutureTask既然是一個Runnable,天然能夠傳遞給Thread,用於任務的封裝,咱們具體看下FutureTask的run方法
細節不看,你會發現run( )方法的調用繞來繞去到了內部的callable的call( )方法調用
image_5c5bc7ee_3079
因此能夠說:
建立Thread實例,有一種途徑,那就是經過new ,藉助於構造方法建立一個Thread類型的對象;
而對於任務的封裝,有兩種方式,一種是繼承Thread,重寫run方法;另一種是實現Runnable接口,將實例對象傳遞給Thread用來構造;
而對於藉助於Runnable實例對象的方式,又有兩種形式,藉助於Runnable接口或者Callable和FutureTask接口
image_5c5bc7ee_2eb0

總結

本文對Thread的構造方法進行了詳細的介紹,儘管構造方法個數不少,可是邏輯很清晰
造方法藉助於底層的init方法完成初始化的封裝邏輯
這是一種優秀的規範--構造方法中不涉及初始化邏輯,若是須要能夠進行封裝。
對於構造方法中用到的各類屬性進行了介紹,列出來了構建一個Thread的最大屬性集合以及最小屬性集合。
start方法和run方法自己有天壤之別,可是對於新人或許卻容易混淆,其實多瞭解一下源代碼以及API文檔就行了
start和run方法運用了模板方法模式,也是一種很好地編程思惟,將不變和變化進行解耦
對於Thread的建立,只有一種方式,那就是new對象(一家之言)
可是對於任務的封裝,卻有兩種方式,之因此這兩種方式,是由run方法的實現決定的,run就表明任務,任務就是run方法,因此就只能替換掉run,可是你又會發現他是個空方法除非target不爲null,因此咱們又能夠替換掉target,因此就有了這兩種方式
對於第二種方式,又有兩種形式直接實現Runnable封裝任務或者經過Callable和FutureTask配合,後者是1.5後的能夠返回執行結果,後續會介紹。
 
對於代碼世界中的不少事情,你說、我說、他說,都不如您看一眼源代碼,提及來彎彎繞的東西,還不是別人寫的代碼?
相關文章
相關標籤/搜索