Java底層分析多線程行爲

做者:享學課堂King老師

多線程是指同時執行多個線程以提升應用程序的性能。java

例如,處理大量信息的框架(如Spring批處理)使用線程來管理數據。同時操做線程或CPU進程能夠提升性能,從而獲得更快、更高效的程序。redis

第一個線程: main() 方法

即便你從未直接使用線程,你也在間接使用它,由於main()方法包含一個主線程。不管什麼時候執行該main()方法,你都執行了主線程。tomcat

咱們能夠經過調用currentThread().getName()方法來訪問正在執行的線程,以下所示:bash

public class MainThread {

public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}

}
複製代碼

此代碼將打印「main」,標識當前正在執行的線程。這是學習多線程概念的第一步。多線程

Java線程生命週期

使用線程時,瞭解線程狀態相當重要。Java的線程生命週期包含六種線程狀態:架構

  • New:Thread()已經實例化了一個新的。併發

  • Runnable接:本Thread的start()方法被調用。負載均衡

  • Running:start()已調用該方法而且線程正在運行。框架

  • Suspended:線程暫時掛起,能夠由另外一個線程恢復。分佈式

  • Blocked:線程正在等待機會運行。當一個線程已經調用該synchronized()方法而且下一個線程必須等到它完成時,就會發生這種狀況。

  • Terminated:線程的執行完成。


併發多線程處理:擴展Thread類

最簡單的是,經過擴展Thread類來完成併發處理,以下所示。

public class InheritingThread extends Thread {

InheritingThread(String threadName) {
super(threadName);
}

public static void main(String... inheriting) {
System.out.println(Thread.currentThread().getName() + " is running");

new InheritingThread("inheritingThread").start();
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running");
}
}
複製代碼

在這裏,咱們運行兩個線程:MainThread和InheritingThread。當咱們start()使用new 調用方法時inheritingThread(),將run()執行方法中的邏輯。

咱們還在Thread類構造函數中傳遞第二個線程的名稱,所以輸出將是:

main is running.inheritingThread is running.
複製代碼

併發多線程處理:Runnable接口

你也能夠實現Runnable接口,而不是使用繼承。Runnable在Thread構造函數內部傳遞會致使更少的耦合和更大的靈活性。傳遞以後Runnable,咱們能夠start()像上一個示例中那樣調用方法:

public class RunnableThread implements Runnable {

public static void main(String... runnableThread) {
System.out.println(Thread.currentThread().getName());

new Thread(new RunnableThread()).start();
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}

}
複製代碼

非守護進程vs守護進程線程

在執行方面,有兩種類型的線程:

  • 執行非守護程序線程會一直到結束。主線程自己就是非守護程序線程的一個很好的例子。main()除非System.exit()強制程序完成,不然代碼輸入將始終執行到最後。

  • 一個守護線程是相反的,是一個不須要一直執行到結束的處理程序。

請記住規則:若是封閉的非守護程序線程在守護程序線程以前結束,則守護程序線程將在結束以前執行。爲了更好地理解守護進程和非守護進程線程的關係,請參考如下示例:

import java.util.stream.IntStream;

public class NonDaemonAndDaemonThread {

public static void main(String... nonDaemonAndDaemon) throws InterruptedException {
System.out.println("Starting the execution in the Thread " + Thread.currentThread().getName());

Thread daemonThread = new Thread(() -> IntStream.rangeClosed(1, 100000)
.forEach(System.out::println));

daemonThread.setDaemon(true);
daemonThread.start();

Thread.sleep(10);

System.out.println("End of the execution in the Thread " +
Thread.currentThread().getName());
}

}
複製代碼

在這個例子中,我使用了守護程序線程來聲明1到100,000的範圍,迭代全部這些,而後打印。但請記住,若是非守護進程的主線程首先完成,守護程序線程將沒法完成執行。

輸出將按以下方式進行:

  1. 在主線程中開始執行。

  2. 打印數字從1到100,000。

  3. 主線程中的執行結束,極可能在迭代到100,000以前完成。

最終輸出將取決於你的JVM實現。

事實證實:線程是不可預測的。

線程優先級和JVM

可使用該setPriority方法肯定線程執行的優先級,可是如何處理它取決於JVM實現。Linux,MacOS和Windows都有不一樣的JVM實現,每一個都將根據本身的默認值處理線程優先級。

可是,你設置的線程優先級確實會影響線程調用的順序。在Thread類上的三個常數是:

/** * The minimum priority that a thread can have. */
public static final int MIN_PRIORITY = 1;

/** * The default priority that is assigned to a thread. */
public static final int NORM_PRIORITY = 5;

/** * The maximum priority that a thread can have. */
public static final int MAX_PRIORITY = 10;
複製代碼

嘗試對如下代碼運行一些測試,以查看最終的執行優先級

public class ThreadPriority {

public static void main(String... threadPriority) {
Thread moeThread = new Thread(() -> System.out.println("Moe"));
Thread barneyThread = new Thread(() -> System.out.println("Barney"));
Thread homerThread = new Thread(() -> System.out.println("Homer"));

moeThread.setPriority(Thread.MAX_PRIORITY);
barneyThread.setPriority(Thread.NORM_PRIORITY);
homerThread.setPriority(Thread.MIN_PRIORITY);

homerThread.start();
barneyThread.start();
moeThread.start();
}

}
複製代碼

即便咱們設置moeThread爲MAX_PRIORITY,咱們也不能期望首先執行此線程。相反,執行順序將是隨機的。

可是,使用常量有一個問題:若是傳遞的優先級數字不在1到10的範圍內,setPriority()方法將引起IllegalArgumentException。因此咱們可使用枚舉來解決這個問題。使用枚舉Enums既簡化了代碼,又可以更好地控制代碼的執行。

Java線程挑戰!

咱們已經瞭解了很多關於線程的知識,若是你想更進一步,研究如下代碼:

public class ThreadChallenge {
private static int line = 10;

public static void main(String... doYourBest) {
new tool("Car").start();

tool Bike = new tool("Bike");
Bike.setPriority(Thread.MAX_PRIORITY);
Bike.setDaemon(false);
Bike.start();

tool Train = new tool("Train");
Train.setPriority(Thread.MIN_PRIORITY);
Train.start();
}

static class tool extends Thread {
tool(String Name) { super(Name); }

@Override public void run() {
line++;
if (line == 13) {
System.out.println(this.getName());
}
}
}
}
複製代碼

這段代碼的輸出是什麼?根據你上面學的內容分析代碼。

A. Car

B. Bike

C. Train

D. 不肯定

解析上述代碼,瞭解多線程的運行

在上面的代碼中,咱們建立了三個線程。第一個線程是Car,咱們爲此線程分配了默認優先級。第二個線程是Bike,分配了MAXPRIORITY優先級。第二個線程是Train,分配了MINPRIORITY優先級。而後咱們開始了多線程。爲了肯定線程將運行的順序,您可能首先注意到tool類擴展了Thread類,而且咱們已經在構造函數中傳遞了線程名稱。咱們還run()中若是line等於13就進行打印。注意!即便Train是咱們執行順序中的第三個線程,而且MIN_PRIORITY不能保證它將在全部JVM實現的最後執行。您可能還會注意到,在此示例中,咱們將Bike線程設置爲守護,因此它是一個守護程序線程,Bike可能永遠不會完成執行。可是其餘兩個線程默認是非守護進程,所以Car和Train線程確定會完成它們的執行。總之,結果將是D:不肯定,由於沒法保證線程調度程序將遵循咱們的執行順序或線程優先級。請記住,若是不借助JUC的工具,咱們不能依賴程序邏輯(線程或線程優先級的順序)來預測線程的執行順序。

多線程常見錯誤

  • 調用run()方法以嘗試啓動新線程。

  • 試圖啓動一個線程兩次(這將致使一個IllegalThreadStateException)。

  • 容許多個進程在不該更改時更改對象的狀態。

  • 編寫依賴於線程優先級的程序邏輯

  • 依賴於線程執行的順序 - 即便咱們首先啓動一個線程,也不能保證它將首先被執行。

關於多線程要記住什麼

  • 調用start()方法啓動一個 Thread。

  • 能夠Thread直接擴展類以使用線程。

  • 能夠在Runnable接口內實現線程動做。

  • 線程優先級取決於JVM實現。

  • 線程行爲將始終取決於JVM實現。

  • 若是封閉的非守護程序線程首先結束,則守護程序線程將沒法完成。

推薦閱讀:

你知道redis企業實戰存在的問題嗎?

你所不知道的Redis熱點問題以及如何發現熱點

你有效地管理JVM的垃圾了嗎?是時候把垃圾拿出來了!

大型網站演變中的負載均衡場景

你知道怎麼在生產環境下部署tomcat嗎?

分佈式之API接口返回格式如何優雅設計?

工做多年,關於線程數存在的誤區

END

歡迎長按下圖關注公衆號:享學課堂online!

公衆號後臺回覆【java】,獲取精選準備的架構學習資料(視頻+文檔+架構筆記)

相關文章
相關標籤/搜索