1. 程式人生 > >菜雞的Java筆記 第三十七 執行緒與程序

菜雞的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 唯一的好處是多了返回值的資料