寫在前面
今天繼續講Java中的程序和執行緒的知識!
程序和執行緒概述
-
程序
程序是正在執行的程式,是系統進行資源分配和呼叫的獨立單位。每一個程序都有它自己的記憶體空間和系統資源。
-
執行緒
執行緒是程序中的單個順序控制流,是一條執行路徑。一個程序如果只有一條執行路徑,則稱為單執行緒程式;如果有多條執行路徑,則稱為多執行緒程式。
Java程式執行原理
Java命令會啟動Java虛擬機器,即JVM,等同於啟動了一個應用程式程序。該程序會自動啟動一個“主執行緒”,然後主執行緒去呼叫某個類的main方法。因此,main方法執行在主執行緒中。在此之前的所有程式都是單執行緒的。
JVM虛擬機器的啟動是單執行緒的還是多執行緒的?
答:JVM的啟動是多執行緒的,包括主執行緒和垃圾回收執行緒等。
多執行緒實現
-
如何建立一個執行緒物件?
- a. 自定義執行緒類繼承Thread類,重寫run方法。
- b. 自定義執行緒類實現Runnable介面,實現run方法。
- c. 自定義執行緒類實現Callable介面,藉助執行緒池,實現run方法。
這裡的run方法是針對一個執行緒物件它所幹的事。
-
如何啟動一個執行緒?
- 呼叫start()方法啟動,Thread類中有start()方法來控制每個執行緒的開始。當然也有stop來控制執行緒的結束。如果單純呼叫run方法則不是使用執行緒的思想來考慮問題,而是簡單的物件呼叫成員方法!
Thread類的基本方法
-
為什麼要重寫run()方法?
- 實現每個執行緒該乾的事。
-
啟動執行緒使用的是哪個方法?
- 使用start()方法啟動執行緒,而不是直接呼叫run()方法。
-
執行緒能不能多次啟動?
- 不能,執行緒一旦啟動就進入了就緒態,之後透過搶佔式來奪取執行權。正在執行當中的執行緒可以透過相關操作進行阻塞回到就緒或者同步該程序。
-
run()和start()方法的區別
- run方法描述了執行緒具體執行的程式碼體,重寫在繼承Thread的子類或實現Runnable介面的類中。而start方法用於啟動一個新執行緒,執行該執行緒的run方法。
Thread類中的成員方法
-
獲取執行緒物件的名字
public final String getName();
-
設定執行緒物件名字的方式
- a. 透過父類的有參構造方法,在建立執行緒物件的時候設定名字。
- b. 執行緒物件呼叫setName(String name)方法,給執行緒物件設定名字。
-
獲取執行緒的優先順序
public final int getPriority();
-
設定執行緒優先順序
public final void setPriority(int i);
- 在啟動之前設定,優先順序範圍為1到10。
-
如何獲取main方法所在的執行緒名稱?
- 使用靜態方法
Thread.currentThread().getName()
,這樣可以獲取任意方法所在的執行緒名稱。
- 使用靜態方法
Runnable介面
- 如何獲取執行緒名稱
- 如何給執行緒設定名稱
實現Runnable介面的好處:
- 可以避免由於Java單繼承帶來的侷限性。
- 適合多個相同程式的程式碼去處理同一個資源的情況,把執行緒同程式的程式碼、資料有效分離,較好地體現了物件導向的設計思想。
Callable介面
- 和執行緒池執行Runnable物件的差不多。
- 好處:
- 可以有返回值。
- 可以丟擲異常。
- 弊端:
- 程式碼比較複雜,所以一般不用。
注意
- 啟動一個執行緒的時候,若直接呼叫run方法,僅僅是普通的物件呼叫方法,底層不會額外建立一個執行緒再執行。
- 從執行的結果上來看,Java執行緒之間是搶佔式執行的,誰先搶到CPU執行權誰就先執行。
- 每次執行的結果順序不可預測,完全隨機的。
- 每個執行緒都有優先權。具有較高優先順序的執行緒優先於優先順序較低的執行緒執行。
排程模型
Java執行緒有兩種排程模型:
-
分時排程模型
- 所有執行緒輪流使用CPU的使用權,平均分配每個執行緒佔用CPU的時間片。
-
搶佔式排程模型
- 優先讓優先順序高的執行緒使用CPU,如果執行緒的優先順序相同,則隨機選擇一個執行緒執行。優先順序高的執行緒獲取的CPU時間片相對多一些。
注意
- Java使用的是搶佔式排程模型。
演示如何設定和獲取執行緒優先順序
public final int getPriority();
public final void setPriority(int newPriority);
設定執行緒優先順序透過setPriority(int i)方法,在啟動之前設定,優先順序範圍為1到10。
物件執行緒控制方法
-
執行緒休眠
public static void sleep(long millis);
-
執行緒加入
public final void join();
-
執行緒禮讓
public static void yield();
-
後臺執行緒
public final void setDaemon(boolean on);
-
中斷執行緒
public final void stop(); public void interrupt();
執行緒的生命週期
執行緒的生命週期包括以下幾個狀態:
- 新建(New):執行緒物件建立後進入此狀態,尚未開始執行。
- 就緒(Runnable):執行緒呼叫了start()方法後,進入此狀態,等待CPU資源。
- 執行(Running):執行緒獲得CPU時間片後,進入此狀態,實際執行run()方法中的程式碼。
- 阻塞(Blocked):執行緒因等待某個資源而阻塞,例如等待I/O操作或鎖。
- 等待(Waiting):執行緒在等待另一個執行緒的特定條件(如等待通知)時處於此狀態。
- 超時等待(Timed Waiting):執行緒在指定的時間內等待,例如呼叫Thread.sleep()。
- 死亡(Terminated):執行緒完成執行或因異常終止後進入此狀態。
解決執行緒安全問題的基本思想
問題判斷
- 是否是多執行緒環境?
- 是否有共享資料?
- 是否有多條語句操作共享資料?
基本思想
讓程式沒有安全問題的環境。核心思想是確保同一時間只有一個執行緒能操作共享資料。
實現方式
-
同步程式碼塊
- 格式:
synchronized (物件) { // 需要同步的程式碼 }
- 解釋: 同步的根本原因在於鎖住的物件。鎖物件如同鎖的功能,確保同一時間只有一個執行緒能夠執行同步程式碼塊。
- 同步的前提:
- 多個執行緒
- 多個執行緒使用的是同一個鎖物件
- 同步的好處: 解決多執行緒安全問題。
- 同步的弊端: 當執行緒很多時,判斷同步鎖的開銷高,可能降低程式執行效率。
同步程式碼塊的物件可以是:
- 任意物件例項
- 當前物件(
this
) - 類物件(
Class
物件) - 常量物件
注意事項:
- 選擇合適的鎖物件:避免死鎖,推薦使用例項物件或類物件。
- 鎖的粒度:粒度過細會導致效能問題,粒度過粗可能導致不必要的執行緒阻塞。
- 避免死鎖:確保獲取鎖的順序一致。
- 格式:
-
同步方法
- 例項方法: 鎖物件是當前例項 (
this
)。 - 靜態方法: 鎖物件是該類的
Class
物件。
建議: 如果鎖物件是
this
,可以考慮使用同步方法。如果鎖物件是其他物件,建議使用同步程式碼塊。 - 例項方法: 鎖物件是當前例項 (
-
Lock鎖的使用
- 特點: 提供顯式的加鎖和解鎖操作,避免隱式鎖的開銷。
- 介面:
void lock(); void unlock();
- 實現:
ReentrantLock
是常用的實現。import java.util.concurrent.locks.ReentrantLock; public static final ReentrantLock lock1 = new ReentrantLock(); public static final ReentrantLock lock2 = new ReentrantLock();
- 弊端:
- 效率低
- 同步巢狀可能導致死鎖
死鎖問題
- 定義: 兩個或更多執行緒因爭奪資源產生的互相等待現象。
執行緒的等待喚醒機制(生產者消費者模型)
- 需求: 只有當產品池中有資料時消費者才去消費,只有當產品池中沒有資料時生產者才去生產。
- 實現: 使用等待喚醒模式,生產者等待消費者的喚醒才開始生產,消費者等待生產者的喚醒才開始消費。
執行緒的狀態轉換圖
執行緒組和執行緒池
執行緒組
- 定義: Java中使用
ThreadGroup
來表示執行緒組,允許對一批執行緒進行分類管理。 - 獲取執行緒組:
public final ThreadGroup getThreadGroup();
- 設定執行緒分組:
Thread(ThreadGroup group, Runnable target, String name);
執行緒池
- 優點: 提高效能,減少建立和銷燬執行緒的開銷,執行緒池中的執行緒複用。
- JDK5 之前: 手動實現執行緒池。
- JDK5 及以後: 提供
Executors
工廠類來建立執行緒池。- 方法:
public static ExecutorService newCachedThreadPool(); public static ExecutorService newFixedThreadPool(int nThreads); public static ExecutorService newSingleThreadExecutor();
- ExecutorService 方法:
Future<?> submit(Runnable task); <T> Future<T> submit(Callable<T> task);
- 方法:
常用執行緒池
-
固定大小執行緒池
static ExecutorService newFixedThreadPool(int nThreads);
- 重用固定數量的執行緒,適用於執行緒數已知的場景。
-
單執行緒池
static ExecutorService newSingleThreadExecutor();
- 只有一個執行緒執行任務,保證任務順序執行。
-
快取執行緒池
static ExecutorService newCachedThreadPool();
- 根據需要建立新執行緒,重用之前構造的執行緒。
-
排程執行緒池
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
- 用於排程任務的執行(定時或週期性)。
匿名內部類方式使用多執行緒
pool.submit(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
Thread t1 = Thread.currentThread();
System.out.println(t1.getName() + " - " + i);
}
}
});
定時器
- 類:
Timer
和TimerTask
- 方法:
- Timer:
public Timer(); public void schedule(TimerTask task, long delay); public void schedule(TimerTask task, long delay, long period);
- TimerTask:
public abstract void run(); public boolean cancel();
- 使用示例:
Timer timer = new Timer(); timer.schedule(new MyTask(), 10000); // 延遲10秒執行 timer.schedule(new MyTask(), 10000, 2000); // 延遲10秒後,每2秒執行一次
- Timer:
多執行緒面試題
-
多執行緒有幾種實現方案?
- 三種:
- 繼承
Thread
類,重寫run
方法。 - 實現
Runnable
介面,重寫run
方法。 - 實現
Callable
介面,透過執行緒池執行。
- 繼承
- 三種:
-
同步有幾種方式?
- 兩種:
- 使用
synchronized
關鍵字。 - 使用
Lock
介面。
- 使用
- 兩種:
-
啟動一個執行緒是
run()
還是start()
?它們的區別?- 啟動執行緒使用
start()
方法,start()
方法會建立一個新的執行緒,並呼叫run()
方法。直接呼叫run()
方法只是普通方法呼叫,不會建立新執行緒。
- 啟動執行緒使用
-
sleep()
和wait()
方法的區別?sleep()
:執行緒進入睡眠狀態,等待指定時間後自動恢復。wait()
:執行緒進入等待阻塞狀態,必須由其他執行緒呼叫notify()
或notifyAll()
才能恢復。
-
為什麼
wait()
,notify()
,notifyAll()
等方法定義在Object
類中?- 因為這些方法用於物件級別的執行緒協調,不依賴於具體的執行緒型別。
設計模式
建立型模式
-
簡單工廠模式
- 用於建立物件的工廠類。
-
工廠方法模式
- 定義一個建立物件的介面,由子類決定例項化哪一個類。
-
單例模式
- 單例設計思想:保證類在記憶體中只有一個物件。
- 實現方式:
- 餓漢式:類載入時建立唯一例項。
- 懶漢式:透過靜態方法在首次使用時建立例項。
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); // 不安全的執行緒操作 } return instance; }
- 懶漢式存在的執行緒安全問題**:
- 多執行緒環境下可能會出現多個例項。解決方案包括使用雙重檢查鎖(Double-Checked Locking)或使用靜態內部類。
行為型模式
-
觀察者模式(Observer Pattern)
- 定義物件之間的一對多依賴關係,讓多個觀察者物件同時監聽一個主題物件。
- 當主題物件狀態發生變化時,所有依賴於它的觀察者都會收到通知並自動更新。
-
策略模式(Strategy Pattern)
- 定義一系列演算法,將每一個演算法封裝起來,並使它們可以互相替換。
- 策略模式讓演算法的變化不會影響到使用演算法的客戶。
-
模板方法模式(Template Method Pattern)
- 定義一個操作中的演算法骨架,而將一些步驟延遲到子類中。
- 模板方法使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。
-
狀態模式(State Pattern)
- 允許一個物件在其內部狀態改變時改變它的行為。
- 狀態模式讓物件看起來好像修改了它的類。
-
責任鏈模式(Chain of Responsibility Pattern)
- 使多個物件有機會處理請求,從而避免請求的傳送者和接收者之間的耦合。
- 將處理請求的物件鏈起來,直到有一個物件處理請求。
結構型模式
-
介面卡模式(Adapter Pattern)
- 將一個類的介面轉換成客戶希望的另一個介面。
- 使得原本由於介面不相容而無法一起工作的類可以一起工作。
-
裝飾器模式(Decorator Pattern)
- 動態地給一個物件新增一些額外的職責。
- 裝飾器模式相比生成子類更加靈活,能夠在不改變類的結構的情況下擴充套件功能。
-
代理模式(Proxy Pattern)
- 為其他物件提供一種代理以控制對這個物件的訪問。
- 代理模式可以用來實現延遲載入、日誌記錄、許可權控制等功能。
-
橋接模式(Bridge Pattern)
- 將抽象部分與實現部分分離,使它們可以獨立變化。
- 橋接模式透過引入一個橋接介面來解耦抽象層和實現層。
-
組合模式(Composite Pattern)
- 允許將物件組合成樹形結構以表示“部分-整體”的層次結構。
- 組合模式使得客戶端對單個物件和物件集合的使用具有一致性。
-
外觀模式(Facade Pattern)
- 為子系統中的一組介面提供一個統一的高層介面。
- 外觀模式定義了一個更高層的介面,讓子系統更易於使用。
-
享元模式(Flyweight Pattern)
- 使用共享物件來高效地支援大量細粒度的物件。
- 享元模式主要用於減少建立物件的開銷,節省記憶體。
-
門面模式(Facade Pattern)
- 提供一個統一的介面來訪問子系統中的一群介面。
- 使得子系統更易於使用和理解。
總結
多執行緒程式設計涉及到執行緒的生命週期、執行緒安全、執行緒池等多個方面。設計模式在軟體設計中提供瞭解決特定問題的標準化方案,幫助實現程式碼的高效重用和擴充套件性。瞭解這些概念和技術有助於編寫高效、可維護的程式碼。