Java執行緒篇——執行緒的開啟

YKamh發表於2018-10-27

隨著開發專案中業務功能的增加,必然某些功能會涉及到執行緒以及併發程式設計的知識點。筆者就在現在的公司接觸到了很多軟硬體結合和socket通訊的專案了,很多的功能運用到了串列埠通訊程式設計,串列埠通訊程式設計的安卓端就是基於執行緒的方式和硬體保持通訊的。

關於Java執行緒,先了解一下Java執行緒的生命週期和物種基本狀態,先上一張經典的圖

Java執行緒篇——執行緒的開啟

上圖也比較直觀的繪製了關於Java執行緒的生命週期同時也囊括了Java執行緒的重點知識點。

Java執行緒的五種狀態:

新建狀態(New):

當執行緒物件建立後,執行緒即進入新建狀態,如:Thread t = new MyThread();

就緒狀態(Runnable):

當執行緒物件的start()方法(t.start();)呼叫時,此時執行緒進入就緒狀態。處於就緒狀態的執行緒只能說明執行緒此時已經做好準備,隨時等待CPU的排程並不是說 t.start(): 後執行緒就會立馬執行;

執行狀態(Running):

當CPU開始排程已經處於就緒狀態的執行緒時,此時執行緒才真正的開始他的工作,即進入了執行狀態。注意(敲黑板!):就緒狀態是進入到執行狀態的唯一入口。也就是說執行緒想進入執行狀態,那執行緒就必須先處於就緒狀態。

阻塞狀態(Blocked):

處於執行狀態的執行緒處於某種原因呢,暫時放棄了對CPU的使用權,停止了執行,此時也就進入了阻塞狀態,知道執行緒再次進入到就緒狀態,才有機會被CPU呼叫進入到執行狀態。而根據造成阻塞的原因不同,分為了一下三種阻塞:

  • 等待阻塞:執行狀態的執行緒執行到了wate()方法,使執行緒進入了等待阻塞狀態。

  • 同步阻塞:執行緒在獲取synchronized同步鎖失敗(因為鎖被其他執行緒佔用),他就會進入同步阻塞狀態。

  • 其他阻塞:通過執行緒的sleep()和join()或發出了I/O請求,執行緒進入到了阻塞狀態。當sleep()超時、join()等待執行緒終止或超時、I/O處理完畢時,執行緒就會再次進入就緒狀態。

死亡狀態(Dead):

執行緒執行完了或者在執行中因異常退出了run()方法,該執行緒就走完了他的一生了。

Java多執行緒的建立和啟動

Java執行緒有三種常見的基本建立方式

繼承Thread類,重寫其run()方法

class MyThread extends Thread {
     
     private int i = 0;
 
     @Override
     public void run() {
         for (i = 0; i < 100; i++) {
             System.out.println(Thread.currentThread().getName() + " " + i);
         }
     }
 }複製程式碼

public class ThreadTest {
 
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
               Thread mThread1 = new MyThread();// 建立一個新的執行緒myThread1此執行緒進入新建狀態
 
               Thread mThread2 = new MyThread();// 建立一個新的執行緒myThread2此執行緒進入新建狀態
 
               mThread1.start();                 // 呼叫start()方法使得執行緒進入就緒狀態
 
               mThread2.start();                 // 呼叫start()方法使得執行緒進入就緒狀態
            }
        }
    }
}複製程式碼

 啊,正如上述程式碼所示,通過繼承Thread類重寫其run()方法,定義了一個新的執行緒類MyThread,其中我們所重寫的run()方法體內的程式碼就是執行緒需要完成的任務了,專業點的來說我們稱之為執行緒執行體。當建立此執行緒類物件時一個新的執行緒得以建立,並進入到執行緒新建狀態。通過呼叫執行緒物件引用的start()方法,使得該執行緒進入到就緒狀態,此時此執行緒並不一定會馬上得以執行,這取決於CPU排程時機。

實現Runnable介面,並重寫實現其run()方法

不必想太多,這個run()方法同樣是我們的執行緒執行體。重寫完run方法後建立介面例項,並把該例項作為建立執行緒的target建立執行緒。沒看懂沒關係,直接上程式碼。

class MyRunnable implements Runnable {
    private int i = 0;
 
    @Override
    public void run() {
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}複製程式碼

public class ThreadTest {
 
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable(); // 建立一個Runnable實現類的物件
                Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target建立新的執行緒
                Thread thread2 = new Thread(myRunnable);
                thread1.start(); // 呼叫start()方法使得執行緒進入就緒狀態
                thread2.start();
            }
        }
    }
}複製程式碼

 其實跟第一種方法也差不多的。那麼Thread和Runnable之間到底是什麼關係呢?我們再來看一個例子:

public class ThreadTest {
 
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable();
                Thread thread = new MyThread(myRunnable);
                thread.start();
            }
        }
    }
}
 
class MyRunnable implements Runnable {
    private int i = 0;
 
    @Override
    public void run() {
        System.out.println("in MyRunnable run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}
 
class MyThread extends Thread {
 
    private int i = 0;
    
    public MyThread(Runnable runnable){
        super(runnable);
    }
 
    @Override
    public void run() {
        System.out.println("in MyThread run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}複製程式碼

 看起來有點奇怪,其實同樣是可以建立執行緒的。那麼聰明敏銳的你肯定會在腦瓜裡面有個提問:那此時執行緒到底是執行MyRunnable裡面的執行體呢還是執行MyThread裡面的執行體呢?答案是MyThread裡的執行體被執行。因為Thread類本身也是實現了Runnable介面,而run()方法最先是在Runnable介面中定義的方法。

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }複製程式碼

 從上面的程式碼我們不難看出,當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable介面並重寫了run()方法的類中的run()方法。但是上述給到的列子中,由於多型的存在,根本就沒有執行到Thread類中的run()方法,而是直接先執行了執行時型別即MyThread類中的run()方法。

用Callable和Future介面建立執行緒

具體是建立Callable介面的實現類,並實現call()方法。並使用FutureTask類來包裝Callable實現類的物件,且以此FutureTask物件作為Thread物件的target來建立執行緒。有點繞~直接看程式碼吧~

public class ThreadTest {
 
    public static void main(String[] args) {
 
        Callable<Integer> myCallable = new MyCallable();    // 建立MyCallable物件
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable物件
 
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Thread thread = new Thread(ft);   //FutureTask物件作為Thread物件的target建立新的執行緒
                thread.start();                      //執行緒進入到就緒狀態
            }
        }
 
        System.out.println("主執行緒for迴圈執行完畢..");
        
        try {
            int sum = ft.get();            //取得新建立的新執行緒中的call()方法返回的結果
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
 
    }
}
 
 
class MyCallable implements Callable<Integer> {
    private int i = 0;
 
    // 與run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }
 
}複製程式碼
我們很容易看得出來,此時的執行緒執行體不再是run()方法了,取而代之的是call()方法,並且這個方法是有返回的。在建立新的執行緒時,是通過FutureTask來包裝MyCallable物件,同時作為了Thread物件的target。那麼看下FutureTask類的定義:

public class FutureTask<V> implements RunnableFuture<V> {
    /*
        ....
    */
}複製程式碼

public interface RunnableFuture<V> extends Runnable, Future<V> {
     
     void run();
     
 }複製程式碼

這樣我們就能清楚的看得出他們之間的關係。發現FutureTask類實際上是同時實現了Runnable和Future介面,使得他有了雙重特性了,通過Runnable特性,可以作為Thread物件的target,而Future特性,使得其可以取得新建立執行緒中的call()方法的返回值。


執行下此程式,我們發現sum = 4950永遠都是最後輸出的。而“主執行緒for迴圈執行完畢..”則很可能是在子執行緒迴圈中間輸出。由CPU的執行緒排程機制,我們知道,“主執行緒for迴圈執行完畢..”的輸出時機是沒有任何問題的,那麼為什麼sum =4950會永遠最後輸出呢?


原因在於通過ft.get()方法獲取子執行緒call()方法的返回值時,當子執行緒此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。


上述主要講解了三種常見的執行緒建立方式,對於執行緒的啟動而言,都是呼叫執行緒物件的start()方法,需要特別注意的是:不能對同一執行緒物件兩次呼叫start()方法。


執行緒的開啟暫時就講這麼多了,後面的文章還會繼續講述Java執行緒之如何優雅的關閉執行緒。

That's all Thank you~

                                                       ----- End -----

                                                            更多好文

                                                     請掃描下面二維碼

                                                             歡迎關注~

                                       Java執行緒篇——執行緒的開啟


相關文章