Thread作為線程的抽象,Thread的實例用於描述線程,對線程的操縱,就是對Thread實例對象的管理與控制。
創建一個線程這個問題,也就轉換為如何構造一個正確的Thread對象。
構造方法列表
構造方法核心
如前面兩個圖所示,Thread共有8個構造方法
而且所有的構造方法都依賴於init方法
private void init(ThreadGroup g, Runnable target, String name,long stackSize)
所以換一個角度思考,可以認為只有一個構造方法
這“唯一的一個構造方法”調用的是五個參數的init方法
所以說,盡管有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();
所以,ThreadGroup是非必填項,如果不進行設置,會有默認初始值
Runnable target
Runnable用於封裝線程任務
Runnable 是一個接口,只有一個run方法,任務的具體內容封裝在run方法中
這是一個抽象方法,另外注意到在1.8中,他成為了一個函數式接口,也就是說可以使用Lambda表達式直接寫Runnable
另外還需要注意到,Thread實現了Runnable接口,實現了run方法
也就是說,Thread天生自帶一個任務,這個任務是什么?
他的任務就是如果target不為null,那么執行target.run(); 方法
而這個target就是通過構造方法注入進來,構造方法對內部變量target進行設置
當線程創建之后,通過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-”開頭,然后會有一個遞增的序列變化
所以,對於線程名稱,如果不設置對程序的正確性、效率等都不會有任何問題
long stackSize
每個線程都有私有的虛擬機棧,通過這個值可以設置棧空間的大小,內部有屬性stackSize,設置的就是這個值
堆棧大小是虛擬機要為該線程堆棧分配的地址空間的近似字節數
在某些平台上,指定一個較高的 stackSize 參數值可能使線程在拋出 StackOverflowError 之前達到較大的遞歸深度
如果指定一個較低的值將允許較多的線程並發地存在,且不會拋出 OutOfMemoryError(或其他內部錯誤)
stackSize 參數(如果有)的作用具有高度的平台依賴性,某些平台這個值都可能被忽略
如果這個值設置為0表示忽略設置
所以,對於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方法,代碼如下
start方法的調用,將會使使該線程開始執行,Java 虛擬機將會調用該線程的 run 方法,接着就是線程並發的運行了
可以看得出來,start方法並沒有調用run方法,關鍵是在於start0,這是一個native方法,依賴於本地方法,畢竟你JVM再牛逼還是要調用底層,有本事你自己跑起來一個看看?
對run方法的執行也是在start0中觸發的,如果start0正確執行,沒有拋出異常,將會設置標志位started=true;
另外,一個線程如果一旦啟動,再次啟動時會拋出異常
看得到threadStatus用於標志是否還沒有啟動,如果不等於0,說明已經啟動了。
所以說,說到這里,start和run方法有什么區別?
他們就沒什么相似的地方,start用於線程的初始化,並且調度執行run方法封裝的任務,run卻僅僅是封裝了任務代碼的一個普通方法。
一個封裝了線程的初始化邏輯,一個只是單純的任務封裝。
對於Thread中start方法和run方法的設計理念,是不是模板方法模式的應用?模板方法模式的意圖如下:
定義一個操作中的算法的骨架,而將一些步驟延時到子類中
TemplateMethod使得子類可以不改變一個算法的結構即可重新定義算法的某些特定步驟
所有的線程初始化的邏輯是相同的,但是每個線程需要執行的任務是千差萬別的;
在Thread中,start方法構建了初始化的邏輯,而將具體的行為轉移到run方法中。
創建線程
隨便百度一下“java創建線程方式”會出來一大堆文章,有說三種方式,也有的說四種方式(線程池也算一種?)
本人不能說人家的就是錯的,但是至少是不准確的。
前面已經提到過,Thread是Java語言本身對線程的抽象,也就是說在Java中,線程只有一種形式,那就是Thread的實例形式存在。
如何創建一個Thread的實例對象?
只有一個途徑,那就是借助於new
對於線程任務的執行,new Thread之后,調用start方法,會調用Thread的run方法,Thread自帶一個run方法,實現自Runnable接口
所以,對於線程任務的設置,換一個問題就是:“如何改變這個run方法為你需要的任務代碼?”
所以,你可以繼承Thread,重寫run方法,完全替代掉這個方法
Thread的子類仍舊是Thread,通過重寫run方法后將線程任務進行封裝,也就是有了任務代碼,new Thread子類().start()即可。
另外,這個方法很顯然,並沒有做什么,只是透傳到target的run方法,所以如果對target進行設置,也可以達到效果
所以准確的說,設置run方法,封裝任務代碼的途徑有兩種
- 繼承Thread,重寫run方法;
- 通過構造方法傳遞Runnable;
第一種途徑--繼承Thread,重寫run方法
第二種途徑--使用Runnable實例對象構造
對於網上的有些示例,比如下面所示,從第一行“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
其實這根本上也是一種使用Runnable實例構造的另外一種形式,我們分析下這個過程
Callable是一個接口(1.8后函數式接口),包含一個call方法,里面可以用來封裝線程執行任務,不同於run方法,可以用有返回值(具體細節后續會詳細說明)
而FutureTask是個什么東西?看下圖,很顯然,他就是一個Runnable
內部持有一個Callable,可以通過構造方法進行注入
FutureTask futureTask = new FutureTask(myCallable);
FutureTask既然是一個Runnable,自然可以傳遞給Thread,用於任務的封裝,我們具體看下FutureTask的run方法
細節不看,你會發現run( )方法的調用繞來繞去到了內部的callable的call( )方法調用
所以可以說:
創建Thread實例,有一種途徑,那就是通過new ,借助於構造方法創建一個Thread類型的對象;
而對於任務的封裝,有兩種方式,一種是繼承Thread,重寫run方法;另外一種是實現Runnable接口,將實例對象傳遞給Thread用來構造;
而對於借助於Runnable實例對象的方式,又有兩種形式,借助於Runnable接口或者Callable和FutureTask接口
總結
本文對Thread的構造方法進行了詳細的介紹,盡管構造方法個數很多,但是邏輯很清晰
造方法借助於底層的init方法完成初始化的封裝邏輯
這是一種優秀的規范--構造方法中不涉及初始化邏輯,如果需要可以進行封裝。
對於構造方法中用到的各種屬性進行了介紹,列出來了構建一個Thread的最大屬性集合以及最小屬性集合。
start方法和run方法本身有天壤之別,但是對於新人或許卻容易混淆,其實多了解一下源代碼以及API文檔就好了
start和run方法運用了模板方法模式,也是一種很好地編程思維,將不變和變化進行解耦
對於Thread的創建,只有一種方式,那就是new對象(一家之言)
但是對於任務的封裝,卻有兩種方式,之所以這兩種方式,是由run方法的實現決定的,run就代表任務,任務就是run方法,所以就只能替換掉run,但是你又會發現他是個空方法除非target不為null,所以我們又可以替換掉target,所以就有了這兩種方式
對於第二種方式,又有兩種形式直接實現Runnable封裝任務或者通過Callable和FutureTask配合,后者是1.5后的可以返回執行結果,后續會介紹。
對於代碼世界中的很多事情,你說、我說、他說,都不如您看一眼源代碼,說起來彎彎繞的東西,還不是別人寫的代碼?























