文章目錄
- 建立線程
-
- 建立線程的兩種方式(本質)
-
- 繼承Thread類
- 實作Runnable接口(推薦)
- 其他建立線程的方式(表面)
-
- 通過線程池
- 通過Callable 和 FutureTask
- 通過定時器
- 總結
-
- 最準确的描述
- 兩者的本質差別
- 優缺點
- 兩種方法同時使用會怎樣
建立線程
建立線程的本質上隻有繼承Thread類 和 實作Runnable接口兩種方式,其他方式如通過線程池建立線程、通過Callable 和 FutureTask建立線程、通過定時器建立線程等,其本質還是通過上述兩種方式進行建立線程,他們都隻不過是包裝了new Thread( )。
多線程的實作方式,在代碼中寫法千變萬化,但是其本質萬變不離其宗。
建立線程的兩種方式(本質)
繼承Thread類
public class ThreadStyle extends Thread{
//重寫Thread類的run方法
@Override
public void run() {
System.out.println("通過繼承Thread類實作線程");
}
public static void main(String[] args) {
/*
因為繼承Thread類之後重寫了Thread類的run方法,是以這裡調用的是繼承Thread類的子類
重寫的run方法,這裡即ThreadStyle中的run方法
*/
//直接建立繼承Thread的子類的執行個體,然後通過執行個體對象調用start方法開啟線程
ThreadStyle threadStyle = new ThreadStyle();
threadStyle.start();
}
}
實作Runnable接口(推薦)
public class RunnableStyle implements Runnable{
//實作Runnable接口中的run方法
@Override
public void run() {
System.out.println("通過Runnable接口實作線程");
}
public static void main(String[] args) {
/*
@Override
public void run() {
if (target != null) {
target.run();
}
}
是以實作Runnable接口的方式實作線程,最終調用的目标對象的run方法,
這裡即new RunnableStyle()對象的run方法,即上面的run方法
*/
//傳入Runnable接口的實作類對象作為target參數值,然後建立一個Thread對象
Thread thread = new Thread(new RunnableStyle());
thread.start();
}
}
其他建立線程的方式(表面)
通過線程池
/**
* 通過線程池的方式建立線程
* 本質還是通過繼承Thread類和實作Runnable接口兩種方式
*/
public class ThreadPool5 {
public static void main(String[] args) {
/**
* 深入源碼可以看出,線程池建立線程的本質還是通過new Thread的方法
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
*/
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++){
executorService.submit(new Tasktest(){
});
}
}
}
class Tasktest implements Runnable{
@Override
public void run() {
//線程休眠
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//列印目前線程的名字
System.out.println(Thread.currentThread().getName());
}
}
通過Callable 和 FutureTask
- Thread類實作了Runnable接口,且Thread類也由Runnable接口組成(聚合關系);
- RunnableFuture 繼承了 Runnable 和 Future 兩個接口;
- FutureTask 實作了RunnableFuture 接口,并且由Thread類組成(聚合關系),由Callable 組成(聚合關系);
- RunnableFuture 接口繼承了 Runnable接口和Future接口;
- FutureTask 實作了 RunnableFuture 接口;
- FutureTask 由Callable 組成
聚合關系:強調整體與部分的關系,整體由部分構成,比如一個部門有多個員工組成;
與組合關系不同的是,整體和部分不是強依賴的,即使整體不存在了,部分依然存在;
例如: 部門撤銷了,員工依然在;
組合關系:與聚合關系一樣,表示整體由部分構成,比如公司有多個部門組成;
但是組合關系是一種強依賴的特殊聚合關系,如果整體不在了,則部門也不在了;
例如:公司不在了,部門也将不在了;
聚合關系:用一條帶空心菱形箭頭的直線表示;
組合關系:用一條帶實心菱形箭頭的直線表示;
參考:
五分鐘讀懂UML類圖
看懂UML類圖和時序圖
類圖
通過上面的類圖分析,可以知道:通過Callable 和 FutureTask建立線程,實質上底層還是通過繼承Thread 和 實作Runnable接口這兩種方式建立線程。
主要步驟:
- (1)建立Callable接口的實作類,并實作 call() 方法,該 call() 方法将作為線程執行體,并且有傳回值;
- (2)建立Callable實作類的執行個體,使用FutureTask類來包裝Callable對象,該FutureTask對象包裝了該Callable對象的call() 方法的傳回值;
- (3)使用FutureTask對象作為Thread對象的target建立并啟動線程;
- (4)調用FutureTask對象的get() 方法來獲得子線程執行結束後的傳回值;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 通過Callable和FutureTask建立線程
* 萬變不離其宗
*/
public class CallableandFutureTask implements Callable<Integer> {
public static void main(String[] args) {
//建立Callable接口實作類的執行個體
CallableandFutureTask callableandFutureTask = new CallableandFutureTask();
//使用FutureTask包裝Callable對象(其包裝了Callable對象的call()方法的傳回值)
FutureTask<Integer> futureTask = new FutureTask<>(callableandFutureTask);
for (int i = 0; i < 100; i++){
System.out.println(Thread.currentThread().getName()+" 的循環變量i的值 "+i);
if (i == 20){//i等于20的時候開始執行子線程
//将FutureTask類的執行個體作為target傳入Thread類中,建立并啟動線程(類似實作Runnable接口方式建立線程)
new Thread(futureTask,"有傳回值的線程").start();
}
}
try {
//調用FutureTask對象的get()方法來擷取子線程執行結束後的傳回值
System.out.println("子線程的傳回值:"+ futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
// 實作Callable接口的call方法
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
參考:
java建立線程的三種方式及其對比
通過定時器
通過定時器建立線程,本質其實還是通過繼承Thread類 和 Runnable接口兩種方式建立線程
import java.util.Timer;
import java.util.TimerTask;
public class DemoTimerTask {
public static void main(String[] args) {
Timer timer = new Timer();
/*
用于定時按周期做任務
第一個參數是task: 表示執行的任務
第二個參數是delay:表示初始化延時,即初始化延遲多少時間開始執行;
第三個參數是period:表示每個多少時間執行一次任務(周期)
注意:這裡的period表示的是相鄰兩個任務開始之間的時間,是以執行時間不會延後
總結起來就是:啟動後過了delay時間之後,開始以period為間隔執行task任務
還需注意schedule和scheduleAtFixedRate的差別
*/
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},5000,1000);
}
}
說明一下schedule方法和scheduleAtFixedRate方法的差別
- scheduleAtFixedRate:每次執行時間為上一次任務開始起向後推一個period間隔,也就是說下次開始執行時間相對于上一次任務開始的時間間隔,是以執行時間不會延後,但是存在任務并發執行的問題。(period:相鄰兩次任務開始之間的時間間隔)
- schedule:每次執行時間為上一次任務結束後推一個period間隔,也就是說下次開始執行時間相對于上一次任務結束的時間間隔,是以執行時間會不斷延後。(period:上次任務結束之後開始計時,即上次任務結束到下次任務開始之間的間隔)
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},5000,1000);
參考:
Java定時任務排程詳解
總結
最準确的描述
- (1)按Oracle文檔來說,建立線程我們通常可以分為兩類:繼承Thread類 和 實作Runnable接口。
- (2)準确地講,建立線程隻有一種方式那就是構造Thread類,而實作線程的執行單元(實作類裡面的run方法)有兩種方式:
- 實作Runnable接口的 run 方法,并把Runnable執行個體傳給 Thread 類;
- 重寫Thread的 run 方法(繼承 Thread 類);
兩者的本質差別
- 繼承Thread類方式:繼承Thread類的子類必須重寫Thread類中的run方法,是以最終調用的也是重寫之後的run方法;
- 實作Runnable接口方式:實作Runnable接口中的run方法,然後将Runnable接口的實作類對象傳入Thread類,是以最終調用的是Runnable接口的實作類中的run方法;
優缺點
實作Runnable接口方式相比繼承Thread類的優點:
- (1)更有利于代碼解耦合;
- (2)能夠繼承其他類,可拓展性更好;
- (3)更容易共享資源(變量);
繼承Thread類的子類的内部變量不會直接共享
- (4)損耗小;
當要建立一個任務時,如果是繼承Thread類的方式,則需要new一個類的對象,但是這樣做的話損耗比較大,這樣我們每次都需要去建立一個線程,執行完之後還需要去銷毀。但是如果我們采用實作Runnable接口的方式,傳入target,傳入實作Runnable接口的類的執行個體的方法,這樣我們就可以反複地利用同一個線程。比如,線程池就是這樣做的。
總結:
繼承Thread類的方式,線程不可複用;
實作Runnable接口的方式,線程可以複用;
繼承Thread類方式也有幾個小小的好處,但相對于其缺點來說,其優點不值一提:
- (1)在 run() 方法内部擷取目前線程可以直接用 this,而無須用 Thread.currentThread( ) 方法;
- (2)繼承Thread類的子類的内部變量不會直接共享,少數不需要共享變量的場景下使用起來會更加友善;
兩種方法同時使用會怎樣
public class BothRunnableThread {
public static void main(String[] args) {
//使用匿名内部類(兩個:一個是Runnable類,一個是Thread類)
new Thread(new Runnable() {
//實作Runnable接口的run方法
@Override
public void run() {
System.out.println("來自Runnable接口的實作類的run方法!");
}
}){
//重寫父類Thread類的run方法
@Override
public void run() {
System.out.println("來自繼承Thread的子類重寫之後的run方法");
}
}.start();
}
}
輸出結果:
來自繼承Thread的子類重寫之後的run方法
因為繼承Thread類的子類重寫了run方法,調用的時候重寫的run方法會覆寫Runnable接口實作類中實作的run方法,是以最終調用的是重寫的run方法。