菜雞的Java筆記 第三十七 執行緒與程序
執行緒與程序
執行緒與程序的區別
最早的的時候DOS 系統有一個特點:只要電腦有病毒,那麼電腦就宕機了,是因為傳統的DOS 系統屬於單程序的作業系統
即:在同一個時間段內只允許有一個程式執行。
而後來到了window 時代發生了改變,電腦即使有病毒了也可以照常使用,但是會變慢
因為在一個CPU ,一塊資源的情況下,程式利用一些輪轉演算法,可以讓一個資源在一個時間段上可以同時處理多個不同的程式(程序),但是i在一個時間點上只允許有一個程序去執行
在每個程序上可以繼續劃分出若干個執行緒,那麼執行緒的操作一定是要比程序更快的,所以多執行緒的操作效能一定要超過多程序的操作
但是所有的執行緒都一定是要在程序的基礎上進行劃分,所以程序一旦消失,那麼執行緒也會消失
總結
執行緒永遠要依附於程序存在
*/
/* 多執行緒的實現(繼承 Thread 類實現)
實現javac的多執行緒操作
在java中對於多執行緒實現一定要有一個執行緒的主類,而這個執行緒的主類往往是需要操作一些資源
但是對於這個多執行緒主類的實現是有一定的要求:
繼承 Thread 父類
實現 Runnable 介面( Callable 介面)
繼承 Thread 類實現多執行緒
在 java.lang 包中存在有 Thread 類。子類繼承 Thread 類之後需要覆寫 Thread 類中的 run() 方法
那麼這個方法就屬於執行緒的主方法,定義: public void run()
範例:實現執行緒的主體類
class MyThread extends Thread{ private String name; public MyThread(String name) { // TODO Auto-generated constructor stub this.name = name; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { System.out.println(this.name + ",i = "+i); } } }
線上程的主類之中只是將內容輸出10次
但是需要注意的是:所有的多執行緒的執行一定是i併發完成的,即:在同一個時間段會有多個執行緒交替執行
所以為了達到這樣的目的,絕對不能夠直接去呼叫 run() 方法,而是應該呼叫 Thread 類中的 start() 方法啟動多執行緒:public void start()
範例:啟動多執行緒
MyThread mt1 = new MyThread("執行緒A"); MyThread mt2 = new MyThread("執行緒B"); MyThread mt3 = new MyThread("執行緒C"); mt1.start(); mt2.start(); mt3.start();
所有的執行緒都屬於交替執行,本身是沒有固定的執行順序的
思考:為什麼現在啟動多執行緒不使用 run() 方法。而非要使用 start() 方法?
為了方便解釋此問題,,必須開啟 Thread 類中的 start() 原始碼來觀察
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0();
現在的程式碼之中首先可以發現方法會丟擲一個異常: IllegalThreadStateException
但是整個方法裡面沒有使用 throws 宣告,沒有 try...catch 捕獲處理,而之所以會出現這樣的情況是因為此異常屬於 RuntimeException 的子類
java.lang.Object
java.lang.Throwable
java.lang.Exception
java.lang.RuntimeException
java.lang.IllegalArgumentException
java.lang.IllegalThreadStateException
此異常指的是一個執行緒已經呼叫了 start() 方法後uyou重複執行了 start() 方法所造成的問題
在呼叫 start() 方法裡面發現會呼叫 start0() 方法,而 start0() 方法上使用了 native 關鍵字定義,這個關鍵字指的是要呼叫本機的作業系統函式
由於執行緒的啟動需要牽扯到作業系統中的資源分配問題,所以具體的執行緒的啟動應該要根據不同的作業系統有不同的實現
而JVM相當於根據系統中定義的 start0() 方法來個根據不同的作業系統進行該方法的實現,這樣在多執行緒的層次上 start0() 方法名稱不改變
而不同的作業系統上有不同的實現
結論:只有 Thread 類的 start() 方法才能進行作業系統的資源的分配,所以啟動多執行緒的方式永遠就是呼叫 Thread 類的 start() 方法實現
package cn.mysterious.study3; class MyThread extends Thread{ private String name; public MyThread(String name) { // TODO Auto-generated constructor stub this.name = name; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { System.out.println(this.name + ",i = "+i); } } } public class StudyThread { public static void main(String[] args) { // TODO Auto-generated method stub MyThread mt1 = new MyThread("執行緒A"); MyThread mt2 = new MyThread("執行緒B"); MyThread mt3 = new MyThread("執行緒C"); mt1.start(); mt2.start(); mt3.start(); // 下面是 現執行A 後B 再C mt1.run(); mt2.run(); mt3.run(); } }
實現 Runnable 介面
繼承 Thread 類會產生單繼承的侷限操作,所以現在最好的做法是利用介面來解決問題,於是就可以使用 Runnable 介面來完成操作
首先來觀察一下 Runnable 介面的定義結構:
@FunctionalInterface public interface Runnable{ public void run(); }
此時的程式碼使用的是函式式的介面。可以利用 Lamda 表示式完成
範例:按照正常思路實現多執行緒
class MyThread implements Runnable { private String name; public MyThread(String name) { // TODO Auto-generated constructor stub this.name = name; } @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { System.out.println(this.name + ",i = "+i); } } }
如果要想啟動多執行緒依靠只能夠是 Thread 類中的 start() 方法,在之前繼承 Thread 類的時候可以直接將 start() 方法繼承下來繼承使用
但是現在實現的是 Runnable 介面,所以此方法沒有了
於是來觀察 Thread 類中的構造方法: public Thread(Runnable target)
// TODO Auto-generated method stub MyThread mt1 = new MyThread("執行緒A"); MyThread mt2 = new MyThread("執行緒B"); MyThread mt3 = new MyThread("執行緒C"); new Thread(mt1).start(); new Thread(mt2).start(); new Thread(mt3).start();
很多時候為了方便實現,可能直接使用匿名內部類或者是 Lamda 實現程式碼
範例:觀察實現
public class StudyThread { public static void main(String[] args) { String name = "?????"; new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { System.out.println(name + ",i = "+i); } } }).start(); } }
範例:JDK1.8使用 Lamda
public class StudyThread { public static void main(String[] args) { String name = "?????"; new Thread(()->{ for (int i = 0; i < 10; i++) { System.out.println(name + ",i = "+i); } } ).start(); } }
只要給出的是函式式介面基本上就都可以使用 Lamda 表示式或者是方法引用
兩種實現方式的區別(面試題)
對於多執行緒的兩種實現模式:繼承 Thread 類,實現 Runnable 介面,那麼這兩種模式本質上來講,一定使用 Runnable介面實現
這樣可以避免單繼承侷限,但是除了這樣的使用原則之外,還需要清楚這兩種實現方式的聯絡
首先觀察 Thread 類的定義結構:
public class Thread extends Object implements Runnable
可以發現 Thread 類實現了 Runnable介面
通過分析可以發現,整個程式碼的操作中使用的就是一個代理設計模式的結構,但是與傳統的代理設計還有些差別
如果按照傳統的代理設計模式來講,現在如果要想啟動多執行緒理論應該是 run() 方法,但是實質上現在呼叫的是 start() 名稱不符合
之所以會這樣主要是因為長期發展後的產物,最早的時候設計模式就是個夢
除了以上的繼承關聯之外還有一點區別: Runnable 介面實現的多執行緒要比 Thread 類實現的多執行緒更方便的表示出資料共享的概念
範例:希望有三個執行緒進行賣票 -- Thread 實現
package cn.mysterious.study3; class MyThread extends Thread { private int ticket = 5; @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 50; i++) { if (this.ticket > 0) { System.out.println("賣票,ticket = "+ this.ticket --); } } } } public class StudyThread { public static void main(String[] args) { MyThread mt1 = new MyThread(); MyThread mt2 = new MyThread(); MyThread mt3 = new MyThread(); mt1.start(); mt2.start(); mt3.start(); } } /* 結果: 賣票,ticket = 5 賣票,ticket = 4 賣票,ticket = 3 賣票,ticket = 2 賣票,ticket = 1 賣票,ticket = 5 賣票,ticket = 4 賣票,ticket = 3 賣票,ticket = 2 賣票,ticket = 1 賣票,ticket = 5 賣票,ticket = 4 賣票,ticket = 3 賣票,ticket = 2 賣票,ticket = 1
*/
發現現在的三個執行緒各自都在賣著各自的票
範例:使用 Runnable 介面來實現多執行緒
package cn.mysterious.study3; class MyThread implements Runnable { private int ticket = 5; @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 50; i++) { if (this.ticket > 0) { System.out.println("賣票,ticket = "+ this.ticket --); } } } } public class StudyThread { public static void main(String[] args) { MyThread mt = new MyThread(); new Thread(mt).start(); new Thread(mt).start(); new Thread(mt).start(); } }
面試題:請解釋多執行緒的兩種實現方式以及區別?請分別用程式碼驗證
多執行緒需要一個執行緒的主類,這個類要麼繼承 Thread 類,要麼實現 Runnable 介面
使用 Runnable 介面可以比 Thread 類更好的實現資料共享的操作,並且利用 Runnable 介面可以避免單繼承侷限問題
實現 Callable 介面
從JDK1.5之後對於多執行緒的實現多了一個 Callable 介面,在這個接口裡面比 Runnable 介面唯一的強大之處在於它可以返回執行結果
此介面定義在 java.util.concurrent 包中
@FunctionalInterface public interface Callable<V>{ public V call() throws Exception }
這個泛型表示的是返回值型別。call() 方法就相當於 run() 方法
範例:定義執行緒的主題類
class MyThread implements Callable<String> { private int ticket = 5; @Override public String call() { // TODO Auto-generated method stub for (int i = 0; i < 50; i++) { if (this.ticket > 0) { System.out.println("賣票,ticket = "+ this.ticket --); } } return "票賣完了"; } }
但是現在出現了一個問題, Thread 類中並沒有提供接收 Callable 介面的物件操作
所現在如何啟動多執行緒就出現了問題。為了分析出啟動的操作,需要來觀察繼承結構
首先來觀察 java.util.concurrent Class FutureTask<V> 類的定義結構
public class StudyThread { public static void main(String[] args) throws Exception { Callable<String> cal = new MyThread(); FutureTask<String> task = new FutureTask<>(cal); // 取得執行結果 Thread thread = new Thread(task); thread.start(); System.out.println(task.get()); // 取得執行緒主方法的返回值 } }
對於執行緒的第三種實現方式沒有特別的要求
總結
Thread 有單繼承侷限所以不使用,但是所有的執行緒物件一定要通過 Thread 裡中的 start() 方法啟動
Runnable 使用是 可以避免單繼承侷限,所以建議使用此操作
Callable 比 Runnable 唯一的好處是多了返回值的資料