執行緒基礎

      在〈執行緒基礎〉中尚無留言

Hyper Threading

超執行緒, 是Intel開發的技術. 在一顆實體的CPU中, 提供二個邏輯執行緒, 模擬成二顆CPU, 所以如果實體上有二顆CPU, 就會模擬成四個核心. 據官方說法, 效能可提升30%.

假設CPU為四核, 就可模擬出八核, 俗稱四核八緒.

Process

無論CPU是幾核幾緒, 站在程式的觀點, 必需當成只有一顆, 因為工作的分配, 全交由作業系統處理, 我們無法控管.

現今作業系統使用搶占式多工任務(Preemptive multitasking), 無論是Linux, 還是Windows, 都可多工操作. 所以一台電腦看似可以同時執行多支程式. 比如一邊下載程式, 一邊聽音樂, 一邊打Word.

CPU在每個時間點, 只能執行一個行程. 作業系統將時間切成數個小片段, 每執行一支程式, 就會向系統註冊, 並啟動一支行程. 作業系統再按分時處理系統決定每個行程可用的時間片段, 並依排程在每個行程之間快速切換執行. 比如Word執行個500ns, Excel執行300ns, Chrome執行200ns. 當Word執行 500ns後, 就切換到Excel執行 300ns, 然後再切換到Chrome. 所以使用者就會覺的每支程式都是同時在處理.

每個行程擁有獨自的資源, 比如CPU的時間佔有數, Ram. 每個行程不可互相干擾. 一個行程最少會有一個執行緒. 第一個執行緒通常稱為主執行緒.

Thread

上述說明, 每一個行程, 致少會有一個執行緒, 此執行緒若是在視窗程式中, 又程為UI主執行緒. UI主執行緒因為是負責整體畫面的繪制及事件的處理, 所以嚴禁讓主執行緒執行需耗費大量時間的工作, 否則會造成UI畫面卡卡的. 所以, 通常會將需耗費大量時間的工作, 交給其他新的執行緒.

試想一個狀況, 當一支行程啟動, 從網路下載大量的資料, 可能需要花一二個小時. 此時若只能有一個執行緒, 則此執行緒只能慢慢等待資料下載完畢, 才能處理其他的工作. 所以在下載過程中, 執行緒無法處理按下的按鈕, 也不能更新畫面.

此執行緒其實大多的時間都是處於等待的狀態,  就算網路再怎麼快, 對CPU而言, 都只是小case而以.  所以若能讓UI主執行緒專心處理UI的事情, 然後產生一個新的執行緒負責下載的工作, 當沒資料進來時, 新執行緒就Idle, 有資料進來才醒來處理. 這樣就可以一邊處理UI, 一邊下載資料了.

程式的執行瓶頸通常發生於如下狀況, 此時多執行緒就派上用場了

資源共競 : 多個任務等待同一個資源
I/O阻塞 : 等待磁碟或網路資料
未充份利用CPU : 單一執行緒僅使用單一的CPU

Thread與行程的差異

每個行程之間, 不可共用資源, 不可互相干擾. 但執行緒是行程之下的分支. 一個行程可以同時擁有多個執行緒, 而每個執行緒可以共用相同的資源.

Thread static Method

當啟動Java的main方法時, 就會有一支主執行緒開始執行裏面的任務, 而此執行緒有自己的名稱及ID, 所以可用如下static 方法得相關訊息

Thread.currentThread().getName()

取得執行緒名稱. 主執行緒預設為main, 其他執行緒預設為Thread-0, Thread-1‧‧‧

Thread.activeCount()

啟動strart()後的數量

public class ThreadTest1 {
    public static void main(String[] args) {
        System.out.printf("Name:%s, ID:%d\n", 
           Thread.currentThread().getName(), Thread.currentThread().getId());
    }
}
結行結果如下 :
Name:main, ID:1

Thread 物件方法

setName(String), getName(), getId()
isAlive() : thread是否完成
isDaemon(), setDaemon(boolean) : 設定常駐程式
join() : 等待其他thread完成
Thread.currentThread()

以下的方法, 盡量避免使用
setPriority(int), getPriority() : 沒什麼用處
destroy(), resume(), suspend(), stop() : 己取消不能用了

啟動執行緒的方法

繼承Thread

先建立一個MyThread類別, 此類別繼承Thread, 然後將要執行的任務放在 run()方法中. 然後於main中產生MyThread物件, 再對此物件下執 start()指令.

public class App{
    public static void main(String[] args) {
        MyThread t1=new MyThread();
        t1.start();
        System.out.println("Main結束了");
    }
}
class MyThread extends Thread{
    public void run(){
        for (int i=0;i<100;i++){
            System.out.printf("%s:%d\n", Thread.currentThread().getName(), i);
        }
    }
}

可以把上述程式碼想像成請了一個職員(t1), 然後作專職的工作(run)

 Runnable抽離工作

先實作一個Runnable介面, 稱為Job, 然後覆寫run方法, 再產生job物件, 然後將job物件塞進Thread裏, 再使用start()方法啟動.

此法就好比將工作抽離獨立出來, 成為一個Runnable物件. Runnable可以想像成公司的專案, 然後將此專案發包給t1去執行.

public class App{
    public static void main(String[] args) {
        Job job=new Job();
        Thread t1=new Thread(job);
        t1.start();
        Thread t2=new Thread(job);
        t2.start(); 
    }
}
class Job implements Runnable{
    @Override
    public void run(){
        for (int i=0;i<100;i++){
            System.out.printf("%s:%d\n", Thread.currentThread().getName(), i);
        }
    }
}

匿名Runnable

當要執行的工作, 只有一次, 往後不會再被其他執行緒執行時, 就可以使用匿名類別簡化程式

public class ThreadTest1 {
    public static void main(String[] args) {
        new Thread(new Runnable(){
            @Override
            public void run(){
               for (int i=1;i<=100;i++){
                   System.out.printf("%s : %d\n", 
                        Thread.currentThread().getName(), i);
                } 
            }
        }, "t1").start();
    }
}

start/run

要開始啟動新執行緒時, 必需使用start方法. 如果直接調用run方法也不會出錯, 一樣會執行.  但執行者是原本main的主執行緒, 不會產生新的執行緒. 所以一般是不會使用run方法.

Thread 生命周期

在上述的程式中, 可以發現一件事, 就是main都結束了, 但新的執行緒還是在背景中拼命的執行, 如果run是一個無窮迴圈, 則會持續的耗電.

Thread safe

上面的程式中, t1及t2都會由0執行到99, 每個執行緒各自執行自己的. 原因在於使用了區域變數, 每個執行緒有自己的區域變數. 所以總共執行了200次.

使用區域變數的方式, 稱為Thread safe

tread-safe通常不共用資料, 如區域變數, 方法參數(其實就是區域變數), Exception 參數. 另只使用可讀不可寫的資料, 如String物件, final fileds, 也是tread-safe.

上面程式碼保証t1會由 0~99, t2也會由0~99, 只是先後順序會跳而以. 為何會這樣子呢! 因為 i 是區域變數, 所以 t1 stack區會有一份 i 變數, t2 stack 區也會有一份i 變數, 二者是不同的變數. 

Non-Thread safe

如果上述的變數 i 為物件變數, 則i是儲存在Heap區的共用變數, 那麼二支執行緒總共執行100多次, 稱為Non-Thread safe.

為什麼是100多次呢? 因為t1 及 t2有時會重複執行相同的 i, 這是因為在尚未加1時, 執行緒就被退出, 換成下一個執行緒進來執行. 每此抓到的 i 值, 尚未加 1, 所以 i 值就會一樣

class Job implements Runnable{
    int i;
    @Override
    public void run(){
        for (i=0;i<100;i++){
            System.out.printf("%s:%d\n", Thread.currentThread().getName(), i);
        }
    }
}

當共用資料時, 就會產生不一致的行為, 需要避免. 為確保一致性, 需同步化(Synchronized)所有的動作. 幾個同步化的方式如下 : volatile, synchronized. 這些會在下一個章節中詳細說明

Thread.sleep

想讓執行緒變慢, 不要執行那麼快, 可以使用 Thread的static 方法 — sleep(int ms), 傳入的參數為睡眠的毫秒數. 因為執行緒進入睡眠時, 可以在其他執行緒使用interrupt()方法讓它醒過來, 此時被干擾的執行緒就會產生 interruptedException, 所以在進入睡眠時, 需使用 try-catch包含起來.

    public static void main(String[] args) {
        Job job=new Job();
        Thread t1=new Thread(job);
        t1.start();
        try{    
            Thread.sleep(3000);
        } catch (InterruptedException ex) {}
        job.close();
        t1.interrupt();
    }

請注意, sleep是static 方法. 也就是說, 只能使用Thread.sleep()讓自己睡著. 執行緒是無法命令別的執行緒去睡覺的. 

但Java本身的issue, 可以使用t1物件去執行sleep, 如 t1.sleep(), 此時就會讓我們誤以為是叫 t1去睡覺, 結果是main 自己去睡覺

Thread終止

新的執行緒一但被生成, main主執行緒就無法控制它. 那麼當主執行緒結束後, 也想要結束新執行緒, 不想讓新執行緒在背景中持續耗電, 這該怎麼達成呢? 答案請看如下程式碼

在Job中寫一個close的方法, 將runFlag設定成false. 所以當迴圈判斷runFlag為false時, 就可以結束了.

但要判斷runFlag是否為false, 也必需等待新執行緒睡醒了, 才能判斷跟結束. 要是睡了10000秒了, 那還是需等到三個多小時後, 才會結束. 所以記得在main中, 還是要下執 t1.interrupt(), 干擾新執行緒的睡眠, 把它叫醒, 再進行判斷結束.

public class JavaApplication48 {
    public static void main(String[] args) {
        Job job=new Job();
        Thread t1=new Thread(job);
        t1.start();
        try{    
            Thread.sleep(3000);
        } catch (InterruptedException ex) {}
        job.close();
        t1.interrupt();
    }
}
class Job implements Runnable{
    private boolean runFlag=true;
    int index;
    @Override
    public void run(){
        while(runFlag){
            try {
                System.out.printf("%s:%d\n", Thread.currentThread().getName(), index++);
                Thread.sleep(1000);
            } 
            catch (InterruptedException ex) {
                System.out.println("被叫醒了, 即將結束");
            }
        }
    }
    public void close(){
        runFlag=false;
    }
}

執行緒狀態

執行緒的狀態, 可以由下圖來進行說明

New : 起始狀態, new Thread時
Runnable:可執行狀態, 利用start()將執行緒推入Runnable pool中, 等待執行
Running : 執行中
Blocked : 鎖定中 , 使用wait或sleep進入Blocked區
Dead : 死亡, 銷毀

java_thread_lifecycle

上述圖示, 若不好理解, 可使用如下方式記憶

Runnable : 後宮, 眾多嬪妃等待皇上臨幸
Running : 就是皇上
Blocked : 冷宮. 被打入冷宮後, 還是有機會翻身進入後宮. 但切記, 冷宮絕對不會直接被皇上臨幸

注意事項
1. Runnable中的眾多執行緒, 那一個會被選入Running中, 無人知道. 就像皇上翻牌, 誰知他會翻到那一個.
2. 同一時間中, Running只能有一個執行緒被執行. 皇上絕不可能同時臨幸二個嬪妃, 又不是在拍A片.
3. Blocked裏的執行緒還是有機會進入Runnable, 但絕對不是直接進入Running. 因為皇上絕不會選冷宮的嬪妃臨幸的.

t1.isAlive()可查出執行緒的狀態
true : Runnable, Running, Block
false : New, Dead

控制執行緒

生父控制權

生父只可以控制要不要生出子執行緒, 及子執行緒的priority, 如下程式碼

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1=new Thread(new Job(), "t1");
        t1.setPriority(Thread.MAX_PRIORITY);
        t1.start();        
        for (int i=1;i<=100;i++){
            System.out.printf("%s : %d\n", Thread.currentThread().getName(), i);
        } 
    }
}
class Job implements Runnable{
    public void run(){
        for (int i=1;i<=100;i++){
            System.out.printf("%s : %d\n", Thread.currentThread().getName(), i);
        }
    }
}

setPriority裏的參數為優先權, 範圍為 1-10, 10為最高, 常用的常數有
Thread.MAX_PRIORITY : 10
Thread.MIN_PRIORITY : 1
Thread.NOR_PRIORITY : 5

上述說歸說, 就像父親每天求神拜佛保佑女兒進宮後能得到皇上的寵幸. 但決定權還是在皇上手中, 拜了也沒用. 也就是說, setPriority沒啥用處, 用了也是白用.

自主控制權

切記一件事 : 執行緒出生後, 一切由自已作主 , 要sleep, 要yield, 還要join, 都是在自己的run()方法作決定. 比如yield, 就是讓位. 皇上自己要讓位, 才叫讓位. 別人叫皇上讓位, 這叫篡位

join

join是物件方法, 是指定自己要在那個執行緒結束後, 才執行自己的東西. 如下程式碼, t1.join(), 是在main執行緒中下達的, 意思是main要等待t1執行完畢後, 才會執行main後面的工作. 所以結果是t1由1~100後, 才會列印main的1-100.

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1=new Thread(new Job(), "t1");
        t1.start();        
        try {
            t1.join();
        } catch (InterruptedException ex) {}
        for (int i=1;i<=100;i++){
            System.out.printf("%s : %d\n", Thread.currentThread().getName(), i);
        } 
    }
}

sleep

Thread.sleep()為static 方法, 會讓自已進入Blocked區, 睡飽了指定的毫秒數後, 才會回到Runnable區.

如下程式碼, 先說明藍色的部份, Runnable裏, t1指定睡了10ms, 但經過前後的計算, 大部份的時間都是11ms, 証明了睡飽了10ms之後, 不是立即進入Running, 還會有一最時間停留在Runnable之中.

再看紅色部份, 雖說在main中使用t1.sleep(). 但絕對不是由main 叫 t1 去睡覺, 因為生父不能控制子執行緒. 但為何可以使用 t1.sleep() 呢? 因為 sleep 是 static 方法. 需由Thread.sleep()調用,  禁止使用物件調用static 方法. 這是Java設計的缺失(C#是禁止的).

所以紅色的部份, 不是t1去睡覺, 反而是 main 自已去睡覺. t1都執行完畢了, main還睡了好幾秒才醒來作後續的工作.

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1=new Thread(new Job(), "t1");
        t1.start();  
        /*
        try {
            t1.sleep(10000);
        } catch (InterruptedException ex) {}
        */
        for (int i=1;i<=100;i++){
            System.out.printf("%s : %d\n", Thread.currentThread().getName(), i);
        } 
    }
}
class Job implements Runnable{
    public void run(){
        for (int i=1;i<=100;i++){
            //System.out.printf("%s : %d\n", Thread.currentThread().getName(), i);
            long start=System.currentTimeMillis();
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {}
            long time=System.currentTimeMillis()-start;
            System.out.println("Slept for "+time+" ms");
        }
    }
}

sleep中斷

sleep是可以被中斷的, 就像是睡到一半, 被人挖了起來沒睡飽. 如下程式碼中, t1預計是要睡飽10秒的, 但在main中過了三秒後, 就使用t1.interrupt(); 結果t1就進入了Runnable區, 因為睡覺的時間顯示為3001ms.

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1=new Thread(new Job(), "t1");
        t1.start();  
        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {}
        t1.interrupt();
    }
}
class Job implements Runnable{
    public void run(){
        long start=System.currentTimeMillis();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException ex) {}
        long time=System.currentTimeMillis()-start;
        System.out.println("Slept for "+time+" ms");
    }
}

yield

Thread.yield()也是static 方法, 表示要讓自己退出Running區, 然後進入Runnable裏. 不過請注意, 有可能退出到Runnable後, 馬上又被排進了Running了. 下述程式碼中, 因為t1常常自己退出Running, 所以main先完成的機會就比t1大很多.

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1=new Thread(new Job(), "t1");
        t1.start();  
        for (int i=1;i<=100;i++){
            System.out.printf("%s:%d\n", 
                    Thread.currentThread().getName(), i);
        }        
    }
}
class Job implements Runnable{
    public void run(){
        for (int i=1;i<=100;i++){
            System.out.printf("%s:%d\n", 
                    Thread.currentThread().getName(), i);
            if(i%2==0)Thread.yield();
        }
    }
}

stop(), destroy()

因為安全性考量, 此二個方法己完全被禁止

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *