1.執行緒是什麼?
程式:每個程式都有獨立的程式碼和資料空間(程式上下文),程式間的切換會有較大的開銷,一個程式包含1–n個執行緒。
執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小。
同一時刻執行多個程式的能力。每一個任務稱為一個執行緒。可以同時執行一個以上執行緒的程式稱為多執行緒程式。
Java編寫程式都執行在在Java虛擬機器(JVM)中,在JVM的內部,程式的多工是通過執行緒來實現的。每用java命令啟動一個java應用程式,就會啟動一個JVM程式。在同一個JVM程式中,有且只有一個程式,就是它自己。在這個JVM環境中,所有程式程式碼的執行都是以執行緒來執行。
一般常見的Java應用程式都是單執行緒的。比如,用java命令執行一個最簡單的HelloWorld的Java應用程式時,就啟動了一個JVM程式,JVM找到程式程式的入口點main(),然後執行main()方法,這樣就產生了一個執行緒,這個執行緒稱之為主執行緒。當main方法結束後,主執行緒執行完成。JVM程式也隨即退出 。
對於一個程式中的多個執行緒來說,多個執行緒共享程式的記憶體塊,當有新的執行緒產生的時候,作業系統不分配新的記憶體,而是讓新執行緒共享原有的程式塊的記憶體。因此,執行緒間的通訊很容易,速度也很快。不同的程式因為處於不同的記憶體塊,因此程式之間的通訊相對困難。
2.執行緒的生命週期
執行緒是一個動態執行的過程,它也有一個從產生到死亡的過程。
下圖顯示了一個執行緒完整的生命週期。
- 新建狀態:使用 new 關鍵字和 Thread 類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start() 這個執行緒。
- 就緒狀態:當執行緒物件呼叫了start()方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,要等待JVM裡執行緒排程器的排程。
- 執行狀態:如果就緒狀態的執行緒獲取 CPU 資源,就可以執行 run(),此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
- 阻塞狀態:如果一個執行緒執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:
- 等待阻塞:執行狀態中的執行緒執行 wait() 方法,使執行緒進入到等待阻塞狀態。
- 同步阻塞:執行緒在獲取 synchronized 同步鎖失敗(因為同步鎖被其他執行緒佔用)。
- 其他阻塞:通過呼叫執行緒的 sleep() 或 join() 發出了 I/O 請求時,執行緒就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待執行緒終止或超時,或者 I/O 處理完畢,執行緒重新轉入就緒狀態。
- 死亡狀態:一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。
3.如何建立一個執行緒
Java 提供了三種建立執行緒的方法:
- 通過實現 Runnable 介面;
- 通過繼承 Thread 類本身;
- 通過 Callable 和 Future 建立執行緒。
通過實現 Runnable 介面來建立執行緒
建立一個執行緒,最簡單的方法是建立一個實現 Runnable 介面的類。
為了實現 Runnable,一個類只需要執行一個方法呼叫 run(),宣告如下:
package org.java.base.thread;
public class RunnableDemo implements Runnable{
@Override
public void run() {
System.out.println(“我是執行緒”);
}
}
通過繼承Thread來建立執行緒
建立一個執行緒的第二種方法是建立一個新的類,該類繼承 Thread 類,然後建立一個該類的例項。
該方法儘管被列為一種多執行緒實現方式,但是本質上也是實現了 Runnable 介面的一個例項。
package org.java.base.thread;
public class ThreadDemo extends Thread{
@Override
public void run() {
System.out.println(“我是執行緒”);
}
}
Thread 方法
下表列出了Thread類的一些重要方法:
序號 | 方法描述 |
---|---|
1 | public void start() 使該執行緒開始執行;Java 虛擬機器呼叫該執行緒的 run 方法。 |
2 | public void run() 如果該執行緒是使用獨立的 Runnable 執行物件構造的,則呼叫該 Runnable 物件的 run 方法;否則,該方法不執行任何操作並返回。 |
3 | public final void setName(String name) 改變執行緒名稱,使之與引數 name 相同。 |
4 | public final void setPriority(int priority) 更改執行緒的優先順序。 |
5 | public final void setDaemon(boolean on) 將該執行緒標記為守護執行緒或使用者執行緒。 |
6 | public final void join(long millisec) 等待該執行緒終止的時間最長為 millis 毫秒。 |
7 | public void interrupt() 中斷執行緒。 |
8 | public final boolean isAlive() 測試執行緒是否處於活動狀態。 |
測試執行緒是否處於活動狀態。 上述方法是被Thread物件呼叫的。下面的方法是Thread類的靜態方法。
序號 | 方法描述 |
---|---|
1 | public static void yield() 暫停當前正在執行的執行緒物件,並執行其他執行緒。 |
2 | public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。 |
3 | public static boolean holdsLock(Object x) 當且僅當當前執行緒在指定的物件上保持監視器鎖時,才返回 true。 |
4 | public static Thread currentThread() 返回對當前正在執行的執行緒物件的引用。 |
5 | public static void dumpStack() 將當前執行緒的堆疊跟蹤列印至標準錯誤流。 |
通過 Callable 和 Future 建立執行緒
- 1. 建立 Callable 介面的實現類,並實現 call() 方法,該 call() 方法將作為執行緒執行體,並且有返回值。
- 2. 建立 Callable 實現類的例項,使用 FutureTask 類來包裝 Callable 物件,該 FutureTask 物件封裝了該 Callable 物件的 call() 方法的返回值。
- 3. 使用 FutureTask 物件作為 Thread 物件的 target 建立並啟動新執行緒。
- 4. 呼叫 FutureTask 物件的 get() 方法來獲得子執行緒執行結束後的返回值。
package org.java.base.thread;
import java.util.concurrent.Callable;
public class CallableThreadDemo implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(“我是一個Callable實現”);
return 1;
}}
-
建立執行緒的三種方式的對比
- 1. 採用實現 Runnable、Callable 介面的方式建立多執行緒時,執行緒類只是實現了 Runnable 介面或 Callable 介面,還可以繼承其他類。
- 2. 使用繼承 Thread 類的方式建立多執行緒時,編寫簡單,如果需要訪問當前執行緒,則無需使用 Thread.currentThread() 方法,直接使用 this 即可獲得當前執行緒。
有效利用多執行緒的關鍵是理解程式是併發執行而不是序列執行的。例如:程式中有兩個子系統需要併發執行,這時候就需要利用多執行緒程式設計。
通過對多執行緒的使用,可以編寫出非常高效的程式。不過請注意,如果你建立太多的執行緒,程式執行的效率實際上是降低了,而不是提升了。
請記住,上下文的切換開銷也很重要,如果你建立了太多的執行緒,CPU 花費在上下文的切換的時間將多於執行程式的時間!
4.執行緒通訊
正常情況下,每個子執行緒完成各自的任務就可以結束了。不過有的時候,我們希望多個執行緒協同工作來完成某個任務,這時就涉及到了執行緒間通訊了。
執行緒之間通訊方式:
1.是通過共享變數,執行緒之間通過該變數進行協作通訊;
例如:多個執行緒共享同一個變數,要考慮併發的問題
2.通過佇列(本質上也是執行緒間共享同一塊記憶體)來實現消費者和生產者的模式來進行通訊;例如:非同步傳送郵件或者簡訊
5.執行緒同步
java允許多執行緒併發控制,當多個執行緒同時操作一個可共享的資源變數時(如資料的增刪改查),
將會導致資料不準確,相互之間產生衝突,因此加入同步鎖以避免在該執行緒沒有完成操作之前,被其他執行緒的呼叫,
即有synchronized關鍵字修飾的方法。由於java的每個物件都有一個內建鎖,當用此關鍵字修飾方法時, 內建鎖會保護整個方法。在呼叫該方法前,需要獲得內建鎖,否則就處於阻塞狀態。
程式碼如:
public synchronized void save(){}
注: synchronized關鍵字也可以修飾靜態方法,此時如果呼叫該靜態方法,將會鎖住整個類
6.執行緒死鎖
死鎖就是兩個或兩個以上的執行緒被無限的阻塞執行緒之間相互等待所需的資源”>死鎖就是兩個或兩個以上的執行緒被無限的阻塞,執行緒之間相互等待所需的資源。這種情況可能發生在當兩個執行緒嘗試獲取其他資源的鎖,而每個執行緒又陷入無線等待其他資源鎖的釋放,除非一個使用者的程式被終止。
執行緒死鎖可能發生在以下的情況:
- 當兩個執行緒相互呼叫Thread.join();
- 當兩個執行緒使用巢狀的同步塊時,一個執行緒佔用了另一個執行緒的必需的鎖,互相等待時被阻塞,就有可能出現死鎖。
死鎖一般都是由於對共享資源的競爭所引起的。但對共享資源的競爭又不一定就會發生死鎖。
死鎖的發生必需滿足4個必要條件:
- 互斥
- 等待/持有
- 非搶佔
- 形成等待環
本節具體程式碼詳見:gitee.com/jxuasea/max…
PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多