線程狀態轉換以及基本操作

来源:https://www.cnblogs.com/lfs2640666960/archive/2019/08/13/11349069.html
-Advertisement-
Play Games

接下來就應該瞭解如何新建一個線程?線程狀態是怎樣轉換的?關於線程狀態的操作是怎樣的?這篇文章就主要圍繞這三個方面來聊一聊。 ...


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

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。如圖

 

 

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

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

2. 線程狀態轉換

 

線程是會在不同的狀態間進行轉換的,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狀態,狀態轉換可以說是線程的生命周期。另外需要註意的是:

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

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()

兩者主要的區別:

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)

    這樣的異常,但是該線程還是會執行,只不過會當做正常的用戶線程執行。

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

    1. sleep()方法是Thread的靜態方法,而wait是Object實例方法

    2. wait()方法必須要在同步方法或者同步塊中調用,也就是必須已經獲得對象鎖。而sleep()方法沒有這個限制可以在任何地方種使用。另外,wait()方法會釋放占有的對象鎖,使得該線程進入等待池中,等待下一次獲取資源。而sleep()方法只是會讓出CPU並不會釋放掉對象鎖;

    3. sleep()方法在休眠時間達到後如果再次獲得CPU時間片就會繼續執行,而wait()方法必須等待Object.notift/Object.notifyAll通知後,才會離開等待池,並且再次獲得CPU時間片才會繼續執行。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 作者:_liuxx cnblogs.com/liuyh/p/8027833.html 前後端分離模式下,所有的交互場景都變成了數據,傳統業務系統中的許可權控制方案在前端已經不再適用,因此引發了我對許可權的重新思考與設計。對於非前後端分離模式下的許可權思考,看這裡:通用數據許可權的思考與設計 許可權控制到底控制 ...
  • 什麼是建造者模式? 工廠模式聚焦於創建出一個對象,而建造者除此之外還需要為創建的對象賦值。 簡單來說,建造者模式=創建對象+屬性賦值。 建造者模式應用場景 建造者模式適合創建 類中包含多個參數且需要定製化 的情況。 簡單來說,建造者模式的目的就是創造 一條龍服務 :不僅創建出對象,順便給屬性賦值。 ...
  • 簡單來說,通過複製的方式創建對象。 【舉個慄子】:點外賣的收貨地址 ...
  • 和單例模式相似,工廠模式同樣聚焦於在考慮整個軟體構建的情況下合理創建對象,從而保證軟體的擴展性和穩定性。 簡單工廠模式:適用客戶端無需擴展的應用場景 //工廠方法模式:適合客戶端創建單個產品的應用場景 //抽象工廠模式:適合創建多個產品(產品固定)的應用場景 ...
  • SpringApplication 使用靜態方法 使用構造器 使用 builder 1、失敗分析器 初始化實現了 FailureAnalyzer 介面的失敗分析器,可以在啟動失敗時,列印錯誤日誌和解決操作方法。比如啟動埠被占用時列印如下日誌: 2、自定義 Banner 可以將 banner.txt ...
  • 一、++再舉例 因此我們在實際開發過程中如果沒有特殊要求儘量使用++在前面 二、關係運算符 >大於 <小於 >=大於等於 <=小於等於 ==等於 !=不等於 註意:關係運算符的運算結果一定是布爾類型true\false 三、邏輯運算符 &邏輯與 |邏輯或 !邏輯非 ^邏輯異或(兩邊的運算元只要不一樣就 ...
  • 1.1.如何在列表中根據條件篩選數據 1.2.如何在列表中根據條件篩選數據 1.3.如何在集合中根據條件篩選數據 1.4.如何為元祖中的每個元素命名,提高程式可讀性 如下元祖,通過函數判斷年齡和性別,但是這樣代碼可讀性很差,別人並不知道student[1],student[2]代表什麼意思。如何解決 ...
  • 問題起因 Eclipse,Tomcat項目存在已經關閉的project,因此無法啟動。 解決方法 進入Tomcat頁面,右鍵Delete,重新添加Server,對項目重新導入,建立連接。 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...