執行緒的狀態轉換以及基本操作

你聽___發表於2018-04-30

原創文章&經驗總結&從校招到A廠一路陽光一路滄桑

詳情請戳www.codercc.com

執行緒的狀態轉換以及基本操作
在上一篇文章中併發程式設計的優缺點談到了為什麼花功夫去學習併發程式設計的技術,也就是說我們必須瞭解到併發程式設計的優缺點,我們在什麼情況下可以去考慮開啟多個執行緒去實現我們的業務,當然使用多執行緒我們應該著重注意一些什麼,在上一篇文章中會有一些討論。那麼,說了這麼多,無論是針對面試還是實際工作中作為一名軟體開發人員都應該具備這樣的技能。萬事開頭難,接下來就應該瞭解如何新建一個執行緒?執行緒狀態是怎樣轉換的?關於執行緒狀態的操作是怎樣的?這篇文章就主要圍繞這三個方面來聊一聊。

1. 新建執行緒

一個java程式從main()方法開始執行,然後按照既定的程式碼邏輯執行,看似沒有其他執行緒參與,但實際上java程式天生就是一個多執行緒程式,包含了:(1)分發處理髮送給給JVM訊號的執行緒;(2)呼叫物件的finalize方法的執行緒;(3)清除Reference的執行緒;(4)main執行緒,使用者程式的入口。那麼,如何在使用者程式中新建一個執行緒了,只要有三種方式:

  1. 通過繼承Thread類,重寫run方法;

  2. 通過實現runable介面;

  3. 通過實現callable介面這三種方式,下面看具體demo。

     public class CreateThreadDemo {
     
         public static void main(String[] args) {
             //1.繼承Thread
             Thread thread = new Thread() {
                 @Override
                 public void run() {
                     System.out.println("繼承Thread");
                     super.run();
                 }
             };
             thread.start();
             //2.實現runable介面
             Thread thread1 = new Thread(new Runnable() {
                 @Override
                 public void run() {
                     System.out.println("實現runable介面");
                 }
             });
             thread1.start();
             //3.實現callable介面
             ExecutorService service = Executors.newSingleThreadExecutor();
             Future<String> future = service.submit(new Callable() {
                 @Override
                 public String call() throws Exception {
                     return "通過實現Callable介面";
                 }
             });
             try {
                 String result = future.get();
                 System.out.println(result);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             } catch (ExecutionException e) {
                 e.printStackTrace();
             }
         }
     
     }
    複製程式碼

三種新建執行緒的方式具體看以上註釋,需要主要的是:

  • 由於java不能多繼承可以實現多個介面,因此,在建立執行緒的時候儘量多考慮採用實現介面的形式;
  • 實現callable介面,提交給ExecutorService返回的是非同步執行的結果,另外,通常也可以利用FutureTask(Callable callable)將callable進行包裝然後FeatureTask提交給ExecutorsService。如圖:
    FutureTask介面實現關係

另外由於FeatureTask也實現了Runable介面也可以利用上面第二種方式(實現Runable介面)來新建執行緒;

  • 可以通過Executors將Runable轉換成Callable,具體方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。

    2. 執行緒狀態轉換

    執行緒狀態轉換圖

    此圖來源於《JAVA併發程式設計的藝術》一書中,執行緒是會在不同的狀態間進行轉換的,java執行緒執行緒轉換圖如上圖所示。執行緒建立之後呼叫start()方法開始執行,當呼叫wait(),join(),LockSupport.lock()方法執行緒會進入到WAITING狀態,而同樣的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超時等待的功能,也就是呼叫這些方法後執行緒會進入TIMED_WAITING狀態,當超時等待時間到達後,執行緒會切換到Runable的狀態,另外當WAITING和TIMED _WAITING狀態時可以通過Object.notify(),Object.notifyAll()方法使執行緒轉換到Runable狀態。當執行緒出現資源競爭時,即等待獲取鎖的時候,執行緒會進入到BLOCKED阻塞狀態,當執行緒獲取鎖時,執行緒進入到Runable狀態。執行緒執行結束後,執行緒進入到TERMINATED狀態,狀態轉換可以說是執行緒的生命週期。另外需要注意的是:

    • 當執行緒進入到synchronized方法或者synchronized程式碼塊時,執行緒切換到的是BLOCKED狀態,而使用java.util.concurrent.locks下lock進行加鎖的時候執行緒切換的是WAITING或者TIMED_WAITING狀態,因為lock會呼叫LockSupport的方法。

    用一個表格將上面六種狀態進行一個總結歸納。

    JAVA執行緒的狀態

    3. 執行緒狀態的基本操作

    除了新建一個執行緒外,執行緒在生命週期內還有需要基本操作,而這些操作會成為執行緒間一種通訊方式,比如使用中斷(interrupted)方式通知實現執行緒間的互動等等,下面就將具體說說這些操作。

    3.1. interrupted

    中斷可以理解為執行緒的一個標誌位,它表示了一個執行中的執行緒是否被其他執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了一個招呼。其他執行緒可以呼叫該執行緒的interrupt()方法對其進行中斷操作,同時該執行緒可以呼叫 isInterrupted()來感知其他執行緒對其自身的中斷操作,從而做出響應。另外,同樣可以呼叫Thread的靜態方法 interrupted()對當前執行緒進行中斷操作,該方法會清除中斷標誌位。需要注意的是,當丟擲InterruptedException時候,會清除中斷標誌位,也就是說在呼叫isInterrupted會返回false。

    執行緒中斷的方法

    下面結合具體的例項來看一看

    public class InterruptDemo {
        public static void main(String[] args) throws InterruptedException {
            //sleepThread睡眠1000ms
            final Thread sleepThread = new Thread() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    super.run();
                }
            };
            //busyThread一直執行死迴圈
            Thread busyThread = new Thread() {
                @Override
                public void run() {
                    while (true) ;
                }
            };
            sleepThread.start();
            busyThread.start();
            sleepThread.interrupt();
            busyThread.interrupt();
            while (sleepThread.isInterrupted()) ;
            System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted());
            System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted());
        }
    }
    複製程式碼

    輸出結果

    sleepThread isInterrupted: false busyThread isInterrupted: true

    開啟了兩個執行緒分別為sleepThread和BusyThread, sleepThread睡眠1s,BusyThread執行死迴圈。然後分別對著兩個執行緒進行中斷操作,可以看出sleepThread丟擲InterruptedException後清除標誌位,而busyThread就不會清除標誌位。

    另外,同樣可以通過中斷的方式實現執行緒間的簡單互動, while (sleepThread.isInterrupted()) 表示在Main中會持續監測sleepThread,一旦sleepThread的中斷標誌位清零,即sleepThread.isInterrupted()返回為false時才會繼續Main執行緒才會繼續往下執行。因此,中斷操作可以看做執行緒間一種簡便的互動方式。一般在結束執行緒時通過中斷標誌位或者標誌位的方式可以有機會去清理資源,相對於武斷而直接的結束執行緒,這種方式要優雅和安全。

    3.2. join

    join方法可以看做是執行緒間協作的一種方式,很多時候,一個執行緒的輸入可能非常依賴於另一個執行緒的輸出,這就像兩個好基友,一個基友先走在前面突然看見另一個基友落在後面了,這個時候他就會在原處等一等這個基友,等基友趕上來後,就兩人攜手並進。其實執行緒間的這種協作方式也符合現實生活。在軟體開發的過程中,從客戶那裡獲取需求後,需要經過需求分析師進行需求分解後,這個時候產品,開發才會繼續跟進。如果一個執行緒例項A執行了threadB.join(),其含義是:當前執行緒A會等待threadB執行緒終止後threadA才會繼續執行。關於join方法一共提供如下這些方法:

    public final synchronized void join(long millis) public final synchronized void join(long millis, int nanos) public final void join() throws InterruptedException

    Thread類除了提供join()方法外,另外還提供了超時等待的方法,如果執行緒threadB在等待的時間內還沒有結束的話,threadA會在超時之後繼續執行。join方法原始碼關鍵是:

     while (isAlive()) {
        wait(0);
     }
    複製程式碼

    可以看出來當前等待物件threadA會一直阻塞,直到被等待物件threadB結束後即isAlive()返回false的時候才會結束while迴圈,當threadB退出時會呼叫notifyAll()方法通知所有的等待執行緒。下面用一個具體的例子來說說join方法的使用:

    public class JoinDemo {
        public static void main(String[] args) {
            Thread previousThread = Thread.currentThread();
            for (int i = 1; i <= 10; i++) {
                Thread curThread = new JoinThread(previousThread);
                curThread.start();
                previousThread = curThread;
            }
        }
    
        static class JoinThread extends Thread {
            private Thread thread;
    
            public JoinThread(Thread thread) {
                this.thread = thread;
            }
    
            @Override
            public void run() {
                try {
                    thread.join();
                    System.out.println(thread.getName() + " terminated.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    複製程式碼

    輸出結果為:

    main terminated. Thread-0 terminated. Thread-1 terminated. Thread-2 terminated. Thread-3 terminated. Thread-4 terminated. Thread-5 terminated. Thread-6 terminated. Thread-7 terminated. Thread-8 terminated.

    在上面的例子中一個建立了10個執行緒,每個執行緒都會等待前一個執行緒結束才會繼續執行。可以通俗的理解成接力,前一個執行緒將接力棒傳給下一個執行緒,然後又傳給下一個執行緒......

    3.3 sleep

    public static native void sleep(long millis)方法顯然是Thread的靜態方法,很顯然它是讓當前執行緒按照指定的時間休眠,其休眠時間的精度取決於處理器的計時器和排程器。需要注意的是如果當前執行緒獲得了鎖,sleep方法並不會失去鎖。sleep方法經常拿來與Object.wait()方法進行比價,這也是面試經常被問的地方。

    sleep() VS wait()

    兩者主要的區別:

    1. sleep()方法是Thread的靜態方法,而wait是Object例項方法
    2. wait()方法必須要在同步方法或者同步塊中呼叫,也就是必須已經獲得物件鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放佔有的物件鎖,使得該執行緒進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉物件鎖;
    3. sleep()方法在休眠時間達到後如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知後,才會離開等待池,並且再次獲得CPU時間片才會繼續執行。

    3.4 yield

    public static native void yield();這是一個靜態方法,一旦執行,它會是當前執行緒讓出CPU,但是,需要注意的是,讓出的CPU並不是代表當前執行緒不再執行了,如果在下一次競爭中,又獲得了CPU時間片當前執行緒依然會繼續執行。另外,讓出的時間片只會分配給當前執行緒相同優先順序的執行緒。什麼是執行緒優先順序了?下面就來具體聊一聊。

    現代作業系統基本採用時分的形式排程執行的執行緒,作業系統會分出一個個時間片,執行緒會分配到若干時間片,當前時間片用完後就會發生執行緒排程,並等待這下次分配。執行緒分配到的時間多少也就決定了執行緒使用處理器資源的多少,而執行緒優先順序就是決定執行緒需要或多或少分配一些處理器資源的執行緒屬性。

    在Java程式中,通過一個整型成員變數Priority來控制優先順序,優先順序的範圍從1~10.在構建執行緒的時候可以通過**setPriority(int)**方法進行設定,預設優先順序為5,優先順序高的執行緒相較於優先順序低的執行緒優先獲得處理器時間片。需要注意的是在不同JVM以及作業系統上,執行緒規劃存在差異,有些作業系統甚至會忽略執行緒優先順序的設定。

    另外需要注意的是,sleep()和yield()方法,同樣都是當前執行緒會交出處理器資源,而它們不同的是,sleep()交出來的時間片其他執行緒都可以去競爭,也就是說都有機會獲得當前執行緒讓出的時間片。而yield()方法只允許與當前執行緒具有相同優先順序的執行緒能夠獲得釋放出來的CPU時間片。

    4.守護執行緒Daemon

    守護執行緒是一種特殊的執行緒,就和它的名字一樣,它是系統的守護者,在後臺默默地守護一些系統服務,比如垃圾回收執行緒,JIT執行緒就可以理解守護執行緒。與之對應的就是使用者執行緒,使用者執行緒就可以認為是系統的工作執行緒,它會完成整個系統的業務操作。使用者執行緒完全結束後就意味著整個系統的業務任務全部結束了,因此係統就沒有物件需要守護的了,守護執行緒自然而然就會退。當一個Java應用,只有守護執行緒的時候,虛擬機器就會自然退出。下面以一個簡單的例子來表述Daemon執行緒的使用。

    public class DaemonDemo {
        public static void main(String[] args) {
            Thread daemonThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            System.out.println("i am alive");
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            System.out.println("finally block");
                        }
                    }
                }
            });
            daemonThread.setDaemon(true);
            daemonThread.start();
            //確保main執行緒結束前能給daemonThread能夠分到時間片
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    複製程式碼

    輸出結果為:

    i am alive finally block i am alive

    上面的例子中daemodThread run方法中是一個while死迴圈,會一直列印,但是當main執行緒結束後daemonThread就會退出所以不會出現死迴圈的情況。main執行緒先睡眠800ms保證daemonThread能夠擁有一次時間片的機會,也就是說可以正常執行一次列印“i am alive”操作和一次finally塊中"finally block"操作。緊接著main 執行緒結束後,daemonThread退出,這個時候只列印了"i am alive"並沒有列印finnal塊中的。因此,這裡需要注意的是守護執行緒在退出的時候並不會執行finnaly塊中的程式碼,所以將釋放資源等操作不要放在finnaly塊中執行,這種操作是不安全的

    執行緒可以通過setDaemon(true)的方法將執行緒設定為守護執行緒。並且需要注意的是設定守護執行緒要先於start()方法,否則會報

    Exception in thread "main" java.lang.IllegalThreadStateException at java.lang.Thread.setDaemon(Thread.java:1365) at learn.DaemonDemo.main(DaemonDemo.java:19)

    這樣的異常,但是該執行緒還是會執行,只不過會當做正常的使用者執行緒執行。

相關文章