1. 程式人生 > >詳細分析 Java 中實現多執行緒的方法有幾種?(從本質上出發)

詳細分析 Java 中實現多執行緒的方法有幾種?(從本質上出發)

[TOC] # 詳細分析 Java 中實現多執行緒的方法有幾種?(從本質上出發) ## 正確的說法(從本質上出發) - **實現多執行緒的官方正確方法: 2 種。** - Oracle 官網的文件說明 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200926005926464.png) - 方法小結 - 方法一: 實現 Runnable 介面。 - 方法二: 繼承 Thread 類。 - 程式碼示例 ```java /** *

* 實現 Runnable 介面的方式建立執行緒 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 00:34 * @since JDK1.8 */ public class RunnableStyle implements Runnable { @Override public void run() { System.out.println("用 Runnable 方式實現執行緒~~~"); } public static void main(String[] args) { Thread thread = new Thread(new RunnableStyle()); thread.start(); } } ``` ```java /** *

* 繼承 Thread 類的方式建立執行緒 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 00:37 * @since JDK1.8 */ public class ThreadStyle extends Thread { @Override public void run() { System.out.println("用 Thread 方式實現執行緒~~~"); } public static void main(String[] args) { new ThreadStyle().start(); } } ``` - 兩種方式的對比 - **方法一(實現 Runnable 介面)更好。** - 方法二的缺點: 1. 從程式碼的架構去考慮,具體執行的任務也就是 run 方法中的內容,它應該和執行緒的建立、執行的機制也就是 Thread 類是解耦的。所以不應該把他們混為一談。從解耦的角度,方法一更好。 2. 該方法每次如果想新建一個任務,只能去新建一個獨立的執行緒,而新建一個獨立的執行緒這樣的損耗是比較大的,它需要去建立、然後執行,執行完了還要銷燬;而如果使用 Runnable 介面的方式,我們就可以利用執行緒池之類的工具,利用這些工具就可以大大減小這些建立執行緒、銷燬執行緒所帶來的損耗。所以方法一相比於方法二的這一點,好在資源的節約上。 3. 繼承了 Thread 類之後,由於 Java 不支援雙繼承,那麼這個類就無法繼承其他的類了,這大大限制了我們的可擴充套件性。 - 兩種方式的本質區別 - 方法一: 最終呼叫 `target.run;` ,通過以下兩圖可以知道使用這個方法時實際上是傳遞了一個 target 物件,執行了這個物件的 run 方法。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200926010328443.png) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200926010343165.png) - 方法二: run() 整個都被重寫。一旦子類重寫了父類的方法,原有方法會被覆蓋被拋棄,即以下程式碼不會被這次呼叫所採納。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200926010458379.png) - 綜上,兩種方法都是執行了 run 方法,只不過 run 方法的來源不同。 - 同時使用兩種方法會怎樣? - 程式碼演示 ```java /** *

* 同時使用 Runnable 和 Thread 兩種實現執行緒的方式 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 22:38 * @since JDK1.8 */ @SuppressWarnings("all") public class BothRunnableThread { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { System.out.println("我來自 Runnable。。。"); } }) { @Override public void run() { System.out.println("我來自 Thread。。。"); } }.start(); } } ``` - 執行結果 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200926010711647.png) - 分析 1. 首先建立了一個匿名內部類 Thread。傳入了一個 Runnable 物件。 2. 然後重寫了 Thread 的 run 方法。最後啟動執行緒。 3. 因為重寫了 Thread 的 run 方法,所以它父類的 run 方法就被覆蓋掉了,所以即便傳入了 Runnable 物件也不會執行它。 - 總結 - 從以上的分析中,準確的講建立執行緒只有一種方式那就是構造 Thread 類,而實現執行緒的執行單元有兩種方式(run 方法的兩種不同實現情況)。 - 方法一: 實現 Runnable 介面的 run 方法,並把 Runnable 例項傳給 Thread 類,再讓 Thread 類去執行這個 run 方法。 - 方法二: 重寫 Thread 的 run 方法(繼承 Thread 類)。 ## 經典錯誤說法(從本質上出發) 1. ”執行緒池建立執行緒也算是一種新建執行緒的方式。“ - 執行緒池建立執行緒程式碼示例 ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** *

* 執行緒池建立執行緒的方法 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 23:05 * @since JDK1.8 */ public class ThreadPools { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 1000; i++) { // 新增任務 executorService.submit(new Task() {}); } } } class Task implements Runnable { @Override public void run() { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); } } ``` - 執行緒池建立執行緒原始碼(DefaultThreadFactory 中) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/img_convert/3f1d2806ac3a60b60182120b0394eab1.png) - 分析 - 通過執行緒池中的原始碼,可以知道執行緒池建立執行緒的方式本質上也是通過構造 Thread 的方式建立的。所以執行緒池建立執行緒的本質和上文中是一樣的。所以不能簡單的認為執行緒池也是一種新的建立執行緒的方式。 2. ”通過 Callable 和 FutureTask 建立執行緒,也算是一種新建執行緒的方式。“ - 類圖展示 ![image.png](https://img-blog.csdnimg.cn/img_convert/5f5950419bde7d035af61798f2094561.png) ![image.png](https://img-blog.csdnimg.cn/img_convert/6acaae561ab64209293d17f595c84a22.png) - 分析 - 從類圖中可以知道這兩個建立執行緒的本質也是和之前的一樣的。 3. ”無返回值是實現 Runnable 介面,有返回值是實現 Callable 介面,所以 Callable 是新的實現執行緒的方式。“ - 和第 2 點差不多。 4. 定時器是新的實現執行緒的方式。 - 定時器實現執行緒程式碼示例 ```java import java.util.Timer; import java.util.TimerTask; /** *

* 定時器建立執行緒 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 23:48 * @since JDK1.8 */ public class DemoTimmerTask { public static void main(String[] args) { Timer timer = new Timer(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }, 1000, 1000); } } ``` - 分析 - 和前面幾點一樣,定時器建立執行緒的方法最終本質也離不開上文中的那兩類方法。 5. 匿名內部類和 Lambda 表示式的方式建立執行緒是新的建立執行緒方式。 - 實際上也和前面幾點一樣是一個表面現象,本質上還是那兩種方法。 - 使用方式程式碼示例 ```java /** *

* 匿名內部類建立執行緒 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 23:54 * @since JDK1.8 */ public class AnonymousInnerClassDemo { public static void main(String[] args) { // 第一種 new Thread() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }.start(); // 第二種 new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } }).start(); } } ``` ```java /** *

* Lambda 表示式建立執行緒 *

* * @author 踏雪彡尋梅 * @version 1.0 * @date 2020/9/7 - 23:58 * @since JDK1.8 */ public class LambdaDemo { public static void main(String[] args) { new Thread(() -> System.out.println(Thread.currentThread().getName())).start(); } } ``` - 執行結果 ![image.png](https://img-blog.csdnimg.cn/img_convert/6bd1712ca242a3bb740519c57d146bc2.png) ![image.png](https://img-blog.csdnimg.cn/img_convert/e340d301e6753926506362073334bfe2.png) - 總結 - 多執行緒的實現方式,在程式碼中寫法千變萬化,但其本質萬變不離其宗。 ## 常見面試問題 - 有多少種實現執行緒的方法?思路有 5 點。 - 從不同的角度看,會有不同的答案。 - 典型答案是兩種。這兩種方式的對比。 - 從原理來看,兩種本質都是一樣的(都是實現 run 方法)。 - 具體展開說其他方式(程式碼的實現上的不同方式,原理還是基於那兩個本質)。 - 將以上幾點做結論。 - 實現 Runnable 介面和繼承 Thread 類哪種方式更好? - 從程式碼架構角度。(應該去解耦,兩件事情:1.具體的任務即 run 方法中的內容;2.和執行緒生命週期相關的如建立執行緒、執行執行緒、銷燬執行緒即 Thread 類去做的事情) - 新建執行緒的損耗的角度。(繼承 Thread 類,需要新建執行緒、執行完之後還要銷燬,實現 Runnable 介面的方式可以反覆的利用同一個執行緒,比如執行緒池就是這麼做的,用於執行緒生命週期的損耗就減少了) - Java 不支援多繼承的角度。(對於擴充套件性而言) --- **如有寫的不足的,請見諒,請大家多多指