文章目錄
創建線程
創建線程的本質上只有繼承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;
}
}
通過定時器
通過定時器創建線程,本質其實還是通過繼承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方法。