1. 程式人生 > >Java 併發:Thread 類深度解析

Java 併發:Thread 類深度解析

摘要:

  Java 中 Thread類 的各種操作與執行緒的生命週期密不可分,瞭解執行緒的生命週期有助於對Thread類中的各方法的理解。一般來說,執行緒從最初的建立到最終的消亡,要經歷建立、就緒、執行、阻塞 和 消亡 五個狀態。線上程的生命週期中,上下文切換通過儲存和恢復CPU狀態使得其能夠從中斷點恢復執行。結合 執行緒生命週期,本文最後詳細介紹了 Thread 各常用 API。特別地,在介紹會導致執行緒進入Waiting狀態(包括Timed Waiting狀態)的相關API時,筆者會特別關注兩個問題:

  • 客戶端呼叫該API後,是否會釋放鎖(如果此時擁有鎖的話);

  • 客戶端呼叫該API後,是否會交出CPU(一般情況下,執行緒進入Waiting狀態(包括Timed Waiting狀態)時都會交出CPU);

一. 執行緒的生命週期

  Java 中 Thread類 的具體操作與執行緒的生命週期密不可分,瞭解執行緒的生命週期有助於對Thread類中的各方法的理解。

  在 Java虛擬機器 中,執行緒從最初的建立到最終的消亡,要經歷若干個狀態:建立(new)就緒(runnable/start)執行(running)阻塞(blocked)等待(waiting)時間等待(time waiting)消亡(dead/terminated)。在給定的時間點上,一個執行緒只能處於一種狀態,各狀態的含義如下圖所示:

            JVM中執行緒的狀態.png-55kB

  當我們需要執行緒來執行某個子任務時,就必須先建立一個執行緒。但是執行緒建立之後,不會立即進入就緒狀態,因為執行緒的執行需要一些條件(比如程式計數器、Java棧、本地方法棧等),只有執行緒執行需要的所有條件滿足了,才進入就緒狀態。當執行緒進入就緒狀態後,不代表立刻就能獲取CPU執行時間,也許此時CPU正在執行其他的事情,因此它要等待。當得到CPU執行時間之後,執行緒便真正進入執行狀態。執行緒在執行狀態過程中,可能有多個原因導致當前執行緒不繼續執行下去,比如使用者主動讓執行緒睡眠(睡眠一定的時間之後再重新執行)、使用者主動讓執行緒等待,或者被同步塊阻塞,此時就對應著多個狀態:time waiting(睡眠或等待一定的時間)、waiting(等待被喚醒)、blocked(阻塞)。當由於突然中斷或者子任務執行完畢,執行緒就會被消亡。

  實際上,Java只定義了六種執行緒狀態,分別是 New, Runnable, Waiting,Timed Waiting、Blocked 和 Terminated。為形象表達執行緒從建立到消亡之間的狀態,下圖將Runnable狀態分成兩種狀態:正在執行狀態和就緒狀態:                 執行緒的生命週期.jpg-52.8kB

二. 上下文切換

  以單核CPU為例,CPU在一個時刻只能執行一個執行緒。CPU在執行一個執行緒的過程中,轉而去執行另外一個執行緒,這個叫做執行緒 上下文切換(對於程序也是類似)。

  由於可能當前執行緒的任務並沒有執行完畢,所以在切換時需要儲存執行緒的執行狀態,以便下次重新切換回來時能夠緊接著之前的狀態繼續執行。舉個簡單的例子:比如,一個執行緒A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停執行緒A,轉去執行執行緒B,當再次切換回來執行執行緒A的時候,我們不希望執行緒A又從檔案的開頭來讀取。

  因此需要記錄執行緒A的執行狀態,那麼會記錄哪些資料呢?因為下次恢復時需要知道在這之前當前執行緒已經執行到哪條指令了,所以需要記錄程式計數器的值,另外比如說執行緒正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU暫存器的狀態。所以,一般來說,執行緒上下文切換過程中會記錄程式計數器、CPU暫存器狀態等資料。

  實質上, 執行緒的上下文切換就是儲存和恢復CPU狀態的過程,它使得執行緒執行能夠從中斷點恢復執行,這正是有程式計數器所支援的。

  雖然多執行緒可以使得任務執行的效率得到提升,但是由於線上程切換時同樣會帶來一定的開銷代價,並且多個執行緒會導致系統資源佔用的增加,所以在進行多執行緒程式設計時要注意這些因素。

三. 執行緒的建立

  在 Java 中,建立執行緒去執行子任務一般有兩種方式:繼承 Thread 類和實現 Runnable 介面。其中,Thread 類本身就實現了 Runnable 介面,而使用繼承 Thread 類的方式建立執行緒的最大侷限就是不支援多繼承。特別需要注意兩點,

  • 實現多執行緒必須重寫run()方法,即在run()方法中定義需要執行的任務;
  • run()方法不需要使用者來呼叫

              Thread類 的結構.png-3.1kB

執行緒建立的程式碼示例:

public class ThreadTest {
    public static void main(String[] args) {

        //使用繼承Thread類的方式建立執行緒
        new Thread(){
            @Override
            public void run() {
                System.out.println("Thread");
            }
        }.start();

        //使用實現Runnable介面的方式建立執行緒
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable");
            }
        });
        thread.start();

        //JVM 建立的主執行緒 main
        System.out.println("main");
    }
}/* Output: (程式碼的執行結果與程式碼的執行順序或呼叫順序無關)
        Thread
        main
        Runnable
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

  建立好自己的執行緒類之後,就可以建立執行緒物件了,然後通過start()方法去啟動執行緒。注意,run() 方法中只是定義需要執行的任務,並且其不需要使用者來呼叫。當通過start()方法啟動一個執行緒之後,若執行緒獲得了CPU執行時間,便進入run()方法體去執行具體的任務。如果使用者直接呼叫run()方法,即相當於在主執行緒中執行run()方法,跟普通的方法呼叫沒有任何區別,此時並不會建立一個新的執行緒來執行定義的任務。實際上,start()方法的作用是通知 “執行緒規劃器” 該執行緒已經準備就緒,以便讓系統安排一個時間來呼叫其 run()方法,也就是使執行緒得到執行。Thread 類中的 run() 方法定義為:

    /* What will be run. */
    private Runnable target;  // 類 Thread 的成員

    /**
     * If this thread was constructed using a separate <code>Runnable</code> run object, 
     * then that <code>Runnable</code> object's <code>run</code> method is called; otherwise, 
     * this method does nothing and returns. 
     * 
     * Subclasses of <code>Thread</code> should override this method. 
     *
     */
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

四. Thread 類詳解

  Thread 類實現了 Runnable 介面,在 Thread 類中,有一些比較關鍵的屬性,比如name是表示Thread的名字,可以通過Thread類的構造器中的引數來指定執行緒名字,priority表示執行緒的優先順序(最大值為10,最小值為1,預設值為5),daemon表示執行緒是否是守護執行緒,target表示要執行的任務。

            Thread類.png-38.9kB

1、與執行緒執行狀態有關的方法

 1) start 方法

 start() 用來啟動一個執行緒,當呼叫該方法後,相應執行緒就會進入就緒狀態,該執行緒中的run()方法會在某個時機被呼叫。

 2)run 方法

 run()方法是不需要使用者來呼叫的。當通過start()方法啟動一個執行緒之後,一旦執行緒獲得了CPU執行時間,便進入run()方法體去執行具體的任務。注意,建立執行緒時必須重寫run()方法,以定義具體要執行的任務。

           Thread-run.png-26.7kB    一般來說,有兩種方式可以達到重寫run()方法的效果:

  • 直接重寫:直接繼承Thread類並重寫run()方法;

  • 間接重寫:通過Thread建構函式傳入Runnable物件 (注意,實際上重寫的是 Runnable物件 的run() 方法)。

 3)sleep 方法

  方法 sleep() 的作用是在指定的毫秒數內讓當前正在執行的執行緒(即 currentThread() 方法所返回的執行緒)睡眠,並交出 CPU 讓其去執行其他的任務。當執行緒睡眠時間滿後,不一定會立即得到執行,因為此時 CPU 可能正在執行其他的任務。所以說,呼叫sleep方法相當於讓執行緒進入阻塞狀態。該方法有如下兩條特徵:

  • 如果呼叫了sleep方法,必須捕獲InterruptedException異常或者將該異常向上層丟擲;

  • sleep方法不會釋放鎖,也就是說如果當前執行緒持有對某個物件的鎖,則即使呼叫sleep方法,其他執行緒也無法訪問這個物件。

           sleep 定義.png-31.6kB

 4)yield 方法

  呼叫 yield()方法會讓當前執行緒交出CPU資源,讓CPU去執行其他的執行緒。但是,yield()不能控制具體的交出CPU的時間。需要注意的是,

  • yield()方法只能讓 擁有相同優先順序的執行緒 有獲取 CPU 執行時間的機會;

  • 呼叫yield()方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒狀態,它只需要等待重新得到 CPU 的執行;

  • 它同樣不會釋放鎖

            yield 的定義.png-9.3kB    

public class MyThread extends Thread {

    @Override
    public void run() {
        long beginTime = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000; i++) {
            Thread.yield();             // 將該語句註釋後,執行會變快
            count = count + (i + 1);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("用時:" + (endTime - beginTime) + "毫秒!");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

 5)join 方法     假如在main執行緒中呼叫thread.join方法,則main執行緒會等待thread執行緒執行完畢或者等待一定的時間。詳細地,如果呼叫的是無參join方法,則等待thread執行完畢;如果呼叫的是指定了時間引數的join方法,則等待一定的時間。join()方法有三個過載版本:

public final synchronized void join(long millis) throws InterruptedException {...}
public final synchronized void join(long millis, int nanos) throws InterruptedException {...}
public final void join() throws InterruptedException {...}
  • 1
  • 2
  • 3

  以 join(long millis) 方法為例,其內部呼叫了Object的wait()方法,如下圖:             這裡寫圖片描述

  根據以上原始碼可以看出,join()方法是通過wait()方法 (Object 提供的方法) 實現的。當 millis == 0 時,會進入 while(isAlive()) 迴圈,並且只要子執行緒是活的,宿主執行緒就不停的等待。 wait(0) 的作用是讓當前執行緒(宿主執行緒)等待,而這裡的當前執行緒是指 Thread.currentThread() 所返回的執行緒。所以,雖然是子執行緒物件(鎖)呼叫wait()方法,但是阻塞的是宿主執行緒。

  看下面的例子,當 main執行緒 執行到 thread1.join() 時,main執行緒會獲得執行緒物件thread1的鎖(wait 意味著拿到該物件的鎖)。只要 thread1執行緒 存活, 就會呼叫該物件鎖的wait()方法阻塞 main執行緒,直至 thread1執行緒 退出才會使 main執行緒 得以繼續執行。

//示例程式碼
public class Test {

    public static void main(String[] args) throws IOException  {
        System.out.println("進入執行緒"+Thread.currentThread().getName());
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        thread1.start();
        try {
            System.out.println("執行緒"+Thread.currentThread().getName()+"等待");
            thread1.join();
            System.out.println("執行緒"+Thread.currentThread().getName()+"繼續執行");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    } 

    class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("進入執行緒"+Thread.currentThread().getName());
            try {
                Thread.currentThread().sleep(5000);
            } catch (InterruptedException e) {
                // TODO: handle exception
            }
            System.out.println("執行緒"+Thread.currentThread().getName()+"執行完畢");
        }
    }
}/* Output:
        進入執行緒main
        執行緒main等待
        進入執行緒Thread-0
        執行緒Thread-0執行完畢
        執行緒main繼續執行
 *///~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

  看上面的例子,當 main執行緒 執行到 thread1.join() 時,main執行緒會獲得執行緒物件thread1的鎖(wait 意味著拿到該物件的鎖)。只要 thread1執行緒 存活, 就會呼叫該物件鎖的wait()方法阻塞 main執行緒。那麼,main執行緒被什麼時候喚醒呢?事實上,有wait就必然有notify。在整個jdk裡面,我們都不會找到對thread1執行緒的notify操作。這就要看jvm程式碼了:

作者:cao
連結:https://www.zhihu.com/question/44621343/answer/97640972
來源:知乎

//一個c++函式:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;

//這個函式的作用就是在一個執行緒執行完畢之後,jvm會做的收尾工作。裡面有一行程式碼:ensure_join(this);

該函式原始碼如下:

static void ensure_join(JavaThread* thread) {
    Handle threadObj(thread, thread->threadObj());

    ObjectLocker lock(threadObj, thread);

    thread->clear_pending_exception();

    java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);

    java_lang_Thread::set_thread(threadObj(), NULL);

    //thread就是當前執行緒,就是剛才說的thread1執行緒。
    lock.notify_all(thread);

    thread->clear_pending_exception();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

  至此,thread1執行緒物件鎖呼叫了notifyall,那麼main執行緒也就能繼續跑下去了。

  由於 join方法 會呼叫 wait方法 讓宿主執行緒進入阻塞狀態,並且會釋放執行緒佔有的鎖,並交出CPU執行許可權。結合 join 方法的宣告,有以下三條:

  • join方法同樣會會讓執行緒交出CPU執行許可權;

  • join方法同樣會讓執行緒釋放對一個物件持有的鎖;

  • 如果呼叫了join方法,必須捕獲InterruptedException異常或者將該異常向上層丟擲。  

 6)interrupt 方法

 interrupt,顧名思義,即中斷的意思。單獨呼叫interrupt方法可以使得 處於阻塞狀態的執行緒 丟擲一個異常,也就是說,它可以用來中斷一個正處於阻塞狀態的執行緒;另外,通過 interrupted()方法 和 isInterrupted()方法 可以停止正在執行的執行緒。interrupt 方法在 JDK 中的定義為:

              interrupt 定義.png-18.7kB    interrupted() 和 isInterrupted()方法在 JDK 中的定義分別為:               interrupted以及isInterrupted.png-69.2kB

 下面看一個例子:

public class Test {

    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    } 

    class MyThread extends Thread{
        @Override
        public void run() {
            try {
                System.out.println("進入睡眠狀態");
                Thread.currentThread().sleep(10000);
                System.out.println("睡眠完畢");
            } catch (InterruptedException e) {
                System.out.println("得到中斷異常");
            }
            System.out.println("run方法執行完畢");
        }
    }
}/* Output:
        進入睡眠狀態
        得到中斷異常
        run方法執行完畢
 *///~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

 從這裡可以看出,通過interrupt方法可以中斷處於阻塞狀態的執行緒。那麼能不能中斷處於非阻塞狀態的執行緒呢?看下面這個例子:

public class Test {

    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {}
        thread.interrupt();
    } 

    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(i<Integer.MAX_VALUE){
                System.out.println(i+" while迴圈");
                i++;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

 執行該程式會發現,while迴圈會一直執行直到變數i的值超出Integer.MAX_VALUE。所以說,直接呼叫interrupt() 方法不能中斷正在執行中的執行緒。但是,如果配合 isInterrupted()/interrupted() 能夠中斷正在執行的執行緒,因為呼叫interrupt()方法相當於將中斷標誌位置為true,那麼可以通過呼叫isInterrupted()/interrupted()判斷中斷標誌是否被置位來中斷執行緒的執行。比如下面這段程式碼:

public class Test {
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread = test.new MyThread();
        thread.start();
        try {
            Thread.currentThread().sleep(2000);
        } catch (InterruptedException e) {

        }
        thread.interrupt();
    } 

    class MyThread extends Thread{
        @Override
        public void run() {
            int i = 0;
            while(!isInterrupted() && i<Integer.MAX_VALUE){
                System.out.println(i+" while迴圈");
                i++;
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

 但是,一般情況下,不建議通過這種方式來中斷執行緒,一般會在MyThread類中增加一個 volatile 屬性 isStop 來標誌是否結束 while 迴圈,然後再在 while 迴圈中判斷 isStop 的值。例如:

class MyThread extends Thread{
        private volatile boolean isStop = false;
        @Override
        public void run() {
            int i = 0;
            while(!isStop){
                i++;
            }
        }

        public void setStop(boolean stop){
            this.isStop = stop;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

 那麼,就可以在外面通過呼叫setStop方法來終止while迴圈。

 7)stop方法

 stop() 方法已經是一個 廢棄的 方法,它是一個 不安全的 方法。因為呼叫 stop() 方法會直接終止run方法的呼叫,並且會丟擲一個ThreadDeath錯誤,如果執行緒持有某個物件鎖的話,會完全釋放鎖,導致物件狀態不一致。所以, stop() 方法基本是不會被用到的。

8、執行緒的暫停與恢復

1) 執行緒的暫停、恢復方法在 JDK 中的定義

  暫停執行緒意味著此執行緒還可以恢復執行。在 Java 中,我可以使用 suspend() 方法暫停執行緒,使用 resume() 方法恢復執行緒的執行,但是這兩個方法已被廢棄,因為它們具有固有的死鎖傾向。如果目標執行緒掛起時在保護關鍵系統資源的監視器上保持有鎖,則在目標執行緒重新開始以前,任何執行緒都不能訪問該資源。如果重新開始目標執行緒的執行緒想在呼叫 resume 之前鎖定該監視器,則會發生死鎖。

例項方法 suspend() 在類Thread中的定義:

           suspend 的定義.png-46.2kB         例項方法 resume() 在類Thread中的定義:

           resume 的定義.png-36.3kB

2) 死鎖

  具體地,在使用 suspend 和 resume 方法時,如果使用不當,極易造成公共的同步物件的獨佔,使得其他執行緒無法得到公共同步物件鎖,從而造成死鎖。下面舉兩個示例:

// 示例 1
public class SynchronizedObject {

    public synchronized void printString() {        // 同步方法
        System.out.println("Thread-" + Thread.currentThread().getName() + " begins.");
        if (Thread.currentThread().getName().equals("a")) {
            System.out.println("執行緒a suspend 了...");
            Thread.currentThread().suspend();
        }
        System.out.println("Thread-" + Thread.currentThread().getName() + " is end.");
    }

    public static void main(String[] args) throws InterruptedException {

        final SynchronizedObject object = new SynchronizedObject();     // 兩個執行緒使用共享同一個物件

        Thread a = new Thread("a") {
            @Override
            public void run() {
                object.printString();
            }
        };
        a.start();

        new Thread("b") {
            @Override
            public void run() {
                System.out.println("thread2 啟動了,在等待中(發生“死鎖”)...");
                object.printString();
            }
        }.start();

        System.out.println("main 執行緒睡眠 " + 5 +" 秒...");
        Thread.sleep(5000);
        System.out.println("main 執行緒睡醒了...");

        a.resume();
        System.out.println("執行緒 a resume 了...");
    }
}/* Output:
        Thread-a begins.
        執行緒a suspend 了...
        thread2 啟動了,在等待中(發生死鎖)...
        main 執行緒睡眠 5 秒...
        main 執行緒睡醒了...
        執行緒 a resume 了...
        Thread-a is end.
        Thread-b begins.
        Thread-b is end.
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

  在示例 2 中,特別要注意的是,println() 方法實質上是一個同步方法。如果 thread 執行緒剛好在執行列印語句時被掛起,那麼將會導致 main執行緒中的字串 “main end!” 遲遲不能列印。其中,println() 方法定義如下:

            println 方法.png-13.5kB

// 示例 2
public class MyThread extends Thread {

    private long i = 0;

    @Override
    public void run() {
        while (true) {
            i++;
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.start();
            Thread.sleep(1);
            thread.suspend();
            System.out.println("main end!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

2、執行緒常用操作

 1)、獲得程式碼呼叫者資訊

 currentThread() 方法返回程式碼段正在被哪個執行緒呼叫的資訊。其在 Thread類 中定義如下:

                currentThread.png-8.9kB

  下面的例子給出了 currentThread() 方法的使用方式:

public class CountOperate extends Thread {

    public CountOperate() {
        super("Thread-CO");     // 執行緒 CountOperate 的名字
        System.out.println("CountOperate---begin");
        System.out.println("Thread.currentThread().getName()="
                + Thread.currentThread().getName());        
        System.out.println("this.getName()=" + this.getName());       
        System.out.println("CountOperate---end");
    }

    @Override
    public void run() {
        System.out.println("run---begin");
        System.out.println("Thread.currentThread().getName()="
                + Thread.currentThread().getName());    
        System.out.println("this.getName()=" + this.getName());    
        System.out.println("run---end");
    }

    public static void main(String[] args) {
        CountOperate c = new CountOperate();
        Thread t1 = new Thread(c);
        t1.setName("A");
        t1.start();         
        c.start();         
    }
}/* Output:(輸出結果不唯一)
        CountOperate---begin                             ....... 行 1
        Thread.currentThread().getName()=main            ....... 行 2
        this.getName()=Thread-CO                         ....... 行 3
        CountOperate---end                               ....... 行 4
        run---begin                                      ....... 行 5
        Thread.currentThread().getName()=A               ....... 行 6
        run---begin                                      ........行 7
        Thread.currentThread().getName()=Thread-CO       ....... 行 8
        this.getName()=Thread-CO                         ....... 行 9
        run---end                                        ....... 行 10
        this.getName()=Thread-CO                         ....... 行 11
        run---end                                        ....... 行 12
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

  首先來看前四行的輸出。我們知道 CountOperate 繼承了 Thread 類,那麼 CountOperate 就得到了 Thread類的所有非私有屬性和方法。CountOperate 構造方法中的 super(“Thread-CO”);意味著呼叫了父類Thread的構造器Thread(String name),也就是為 CountOperate執行緒 賦了標識名。由於該構造方法是由main()方法呼叫的,因此此時 Thread.currentThread() 返回的是main執行緒;而 this.getName() 返回的是CountOperate執行緒的標識名。

  其次,在main執行緒啟動了t1執行緒之後,CPU會在某個時機執行類CountOperate的run()方法。此時,Thread.currentThread() 返回的是t1執行緒,因為是t1執行緒的啟動使run()方法得到了執行;而 this.getName() 返回的仍是CountOperate執行緒的標識名,因為此時this指的是傳進來的CountOperate物件(具體原因見上面對run()方法的介紹),由於它本身也是一個執行緒物件,所以可以呼叫getName()得到相應的標識名。

  在main執行緒啟動了CountOperate執行緒之後,CPU也會在某個時機執行類該執行緒的run()方法。此時,Thread.currentThread() 返回的是CountOperate執行緒,因為是CountOperate執行緒的啟動使run()方法得到了執行;而 this.getName() 返回的仍是CountOperate執行緒的標識名,因為此時this指的就是剛剛建立的CountOperate物件本身,所以得到的仍是 “Thread-CO ”。

2)、判斷執行緒是否處於活動狀態

  方法 isAlive() 的功能是判斷呼叫該方法的執行緒是否處於活動狀態。其中,活動狀態指的是執行緒已經 start (無論是否獲得CPU資源並執行) 且尚未結束。                  這裡寫圖片描述

  下面的例子給出了 isAlive() 方法的使用方式:

public class CountOperate extends Thread {

    public CountOperate() {
        System.out.println("CountOperate---begin");

        System.out.println("Thread.currentThread().getName()="
                + Thread.currentThread().getName());        // main
        System.out.println("Thread.currentThread().isAlive()="
                + Thread.currentThread().isAlive());        // true

        System.out.println("this.getName()=" + this.getName());         // Thread-0
        System.out.println("this.isAlive()=" + this.isAlive());         // false

        System.out.println("CountOperate---end");
    }

    @Override
    public void run() {
        System.out.println("run---begin");

        System.out.println("Thread.currentThread().getName()="
                + Thread.currentThread().getName());        // A
        System.out.println("Thread.currentThread().isAlive()="
                + Thread.currentThread().isAlive());        // true

        System.out.println("this.getName()=" + this.getName());     // Thread-0
        System.out.println("this.isAlive()=" + this.isAlive());     // false

        System.out.println("run---end");
    }

    public static void main(String[] args) {
        CountOperate c = new CountOperate();
        Thread t1 = new Thread(c);
        System.out.println("main begin t1 isAlive=" + t1.isAlive());        // false
        t1.setName("A");
        t1.start();
        System.out.println("main end t1 isAlive=" + t1.isAlive());      // true
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

  該程式所反映的知識點與上面的程式類似,此不贅述。 

3)、獲取執行緒唯一標識

  方法 getId() 的作用是取得執行緒唯一標識,由JVM自動給出

              getId 定義.png-17.7kB

// 示例
public class Test {
    public static void main(String[] args) {
        Thread runThread = Thread.currentThread();
        System.out.println(runThread.getName() + " " + runThread.getId());
    }
}/* Output:
        main 1
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

4)、getName和setName

  用來得到或者設定執行緒名稱。如果我們不手動設定執行緒名字,JVM會為該執行緒自動建立一個標識名,形式為: Thread-數字

5)、getPriority和setPriority

  在作業系統中,執行緒可以劃分優先順序,優先順序較高的執行緒得到的CPU資源較多,也就是CPU優先執行優先順序較高的執行緒設定執行緒優先順序有助於幫助 “執行緒規劃器” 確定在下一次選擇哪個執行緒來獲得CPU資源。特別地,在 Java 中,執行緒的優先順序分為 1 ~ 10 這 10 個等級,如果小於 1 或大於 10,則 JDK 丟擲異常 IllegalArgumentException ,該異常是 RuntimeException 的子類,屬於不受檢異常。JDK 中使用 3 個常量來預置定義優先順序的值,如下:

public static final int MIN_PRIORITY = 1; 
public static final int NORM_PRIORITY = 5; 
public static final int MAX_PRIORITY = 10; 
  • 1
  • 2
  • 3

  在 Thread類中,方法 setPriority() 的定義為:

            這裡寫圖片描述

(1). 執行緒優先順序的繼承性

  在 Java 中,執行緒的優先順序具有繼承性,比如 A 執行緒啟動 B 執行緒, 那麼 B 執行緒的優先順序與 A 是一樣的。

class MyThread2 extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread2 run priority=" + this.getPriority());
    }
}

public class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread1 run priority=" + this.getPriority());
        MyThread2 thread2 = new MyThread2();
        thread2.start();
    }

    public static void main(String[] args) {
        System.out.println("main thread begin priority="
                + Thread.currentThread().getPriority());
        Thread.currentThread().setPriority(6);
        System.out.println("main thread end   priority="
                + Thread.currentThread().getPriority());
        MyThread1 thread1 = new MyThread1();
        thread1.start();
    }
}/* Output:
        main thread begin priority=5
        main thread end   priority=6
        MyThread1 run priority=6
        MyThread2 run priority=6
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

(2). 執行緒優先順序的規則性和隨機性

  執行緒的優先順序具有一定的規則性,也就是CPU儘量將執行資源讓給優先順序比較高的執行緒。特別地,高優先順序的執行緒總是大部分先執行完,但並不一定所有的高優先順序執行緒都能先執行完。

6)、守護執行緒 (Daemon)

  在 Java 中,執行緒可以分為兩種型別,即使用者執行緒和守護執行緒。守護執行緒是一種特殊的執行緒,具有“陪伴”的含義:當程序中不存在非守護執行緒時,則守護執行緒自動銷燬,典型的守護執行緒就是垃圾回收執行緒。任何一個守護執行緒都是整個JVM中所有非守護執行緒的保姆,只要當前JVM例項中存在任何一個非守護執行緒沒有結束,守護執行緒就在工作;只有當最後一個非守護執行緒結束時,守護執行緒才隨著JVM一同結束工作。 在 Thread類中,方法 setDaemon() 的定義為:

            這裡寫圖片描述

public class MyThread extends Thread {

    private int i = 0;

    @Override
    public void run() {
        try {
            while (true) {
                i++;
                System.out.println("i=" + (i));
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();
            thread.setDaemon(true);       //設定為守護執行緒
            thread.start();
            Thread.sleep(3000);
            System.out.println("main 執行緒結束,也意味著守護執行緒 thread 將要結束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}/* Output: (結果不唯一)
        i=1
        i=2
        i=3
        main 執行緒結束,也意味著守護執行緒 thread 將要結束
 *///:~
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

三. 小結

1). 對於上述執行緒的各項基本操作,其 所操作的物件 滿足:

  • 若該操作是靜態方法,也就是說,該方法屬於類而非具體的某個物件,那麼該操作的作用物件就是 currentThread() 方法所返回 Thread 物件;

  • 若該操作是例項方法,也就是說,該方法屬於物件,那麼該操作的作用物件就是呼叫該方法的 Thread 物件。

2). 對於上述執行緒的各項基本操作,有:

  • 執行緒一旦被阻塞,就會釋放 CPU;

  • 當執行緒出現異常且沒有捕獲處理時,JVM會自動釋放當前執行緒佔用的鎖,因此不會由於異常導致出現死鎖現象。

  • 對於一個執行緒,CPU 的釋放 與 鎖的釋放沒有必然聯絡。

3). Thread類 中的方法呼叫與執行緒狀態關係如下圖:                   Thread方法與狀態.jpg-72.4kB

引用