1. 程式人生 > 其它 >棧區,堆區,全域性區,靜態區,程式碼區,常量區,自由儲存區

棧區,堆區,全域性區,靜態區,程式碼區,常量區,自由儲存區

多執行緒

程序是程式的依次執行過程,執行緒是比程序更小的執行單位,一個程序在其執行的過程中可以產生多個執行緒,多個執行緒共享程序的堆和方法區記憶體資源。

1.程序和執行緒

程式是含有指令和資料的檔案,是靜態的程式碼,被儲存在磁碟或其他的資料儲存裝置中。

程序是程式的一次執行過程,執行緒是程序劃分成的更小的執行單位。

程序和執行緒的聯絡和區別

程序和執行緒最大的區別是,各程序是獨立的,而各執行緒則不一定獨立,因為同一程序中的多個執行緒極有可能會相互影響。執行緒執行開銷小,但不利於資源的管理和保護,程序則相反。

程序是程式的一次執行過程,是系統執行程式的基本單位,因此程序是動態的。系統執行一個程式即為一個程序的建立、執行以及消亡的過程。

執行緒是比程序更小的執行單位。

一個程序在其執行的過程中可以產生多個執行緒,多個執行緒共享程序的堆和方法區記憶體資源,每個執行緒都有自己的程式計數器、虛擬機器棧和本地方法棧

由於執行緒共享程序的記憶體,因此係統產生一個執行緒或者在多個執行緒之間切換工作時的負擔比程序小得多,執行緒也稱為輕量級程序。

程序和執行緒最大的區別是,各程序是獨立的,而各執行緒則不一定獨立,因為同一程序中的多個執行緒極有可能會相互影響。

執行緒執行開銷小,但不利於資源的管理和保護,程序則相反。

執行緒的狀態

執行緒在執行的生命週期中的任何時刻只能是 6 種不同狀態的其中一種。

  • 初始狀態(NEW):執行緒已經構建,尚未啟動。
  • 執行狀態(RUNNABLE):包括就緒(READY)和執行中(RUNNING)兩種狀態,統稱為執行狀態。
  • 阻塞狀態(BLOCKED):執行緒被鎖阻塞。
  • 等待狀態(WAITING):執行緒需要等待其他執行緒做出特定動作(通知或中斷)。
  • 超時等待狀態(TIME_WAITING):不同於等待狀態,超時等待狀態可以在指定的時間自行返回。
  • 終止狀態(TERMINATED):當前執行緒已經執行完畢。

多執行緒的優點和可能存在的問題

執行緒也稱為輕量級程序,是程式執行的最小單位,執行緒間的切換和排程的成本遠遠小於程序,多個執行緒同時執行可以減少執行緒上下文切換的開銷。

多執行緒是開發高併發系統的基礎,利用好多執行緒機制可以顯著提高系統的併發能力和效能

多執行緒併發程式設計並不總是能提高程式的執行效率和執行速度,而且可能存在一些問題,包括記憶體洩漏、上下文切換、死鎖以及受限於硬體和軟體的資源限制問題等


2.關鍵字 synchronized volatile

關鍵字 synchronized

該關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。

關鍵字 synchronized 解決的是多個執行緒之間訪問資源的同步性。

關鍵字 synchronized 最主要的三種使用方式是:修飾例項方法、修飾靜態方法、修飾程式碼塊。

  • 修飾例項方法:給當前物件例項加鎖,進入同步程式碼之前需要獲得當前物件例項的鎖。
  • 修飾靜態方法:給當前類加鎖,進入同步程式碼之前需要獲得當前類的鎖。
  • 修飾程式碼塊:指定加鎖物件,給指定物件加鎖,進入同步程式碼塊之前需要獲得指定物件的鎖。

關鍵字 volatile

該關鍵字修飾的變數會直接在主記憶體中進行讀寫操作,保證了變數的可見性。

關鍵字 volatile 解決的是變數在多個執行緒之間的可見性。

除了保證變數的可見性以外,關鍵字 volatile 還有一個作用是確保程式碼的執行順序不變。

為了提高執行程式時的效能,編譯器和處理器會對指令進行重排序優化,因此程式碼的執行順序和編寫程式碼的順序可能不一致。

新增關鍵字 volatile 可以禁止指令進行重排序優化。

只有當一個變數滿足以下兩個條件時,才能使用關鍵字 volatile。

  • 對變數的寫入操作不依賴變數的當前值,或者能確保只有單個執行緒更新變數的值。

  • 該變數沒有包含在具有其他變數的不變式中。

volatile修飾的變數會直接在主記憶體中進行讀寫操作,而非各執行緒自己的工作記憶體。

可見性的問題主要原因是因為CPU多核,而且每個核上面都有CPU快取,CPU快取對於不同核來說相互不可見,因此這個指令的作用是讓CPU從記憶體讀取資料,而不是從CPU快取。

volatile關鍵字修飾的變數被改變時, 會強制立即寫入記憶體中, 並且讓其在CPU緩衝區域中對應的緩衝行無效, 其他執行緒讀取這個變數時, 強制其他執行緒從記憶體中重新讀取這個變數的值。

關鍵字 synchronized 和 volatile 的區別

  • 關鍵字 volatile 是執行緒同步的輕量級實現,不需要加鎖,因此效能優於關鍵字 synchronized
  • 關鍵字 synchronized 可以修飾方法和程式碼塊,關鍵字 volatile 只能修飾變數。
  • 關鍵字 synchronized 可能發生阻塞,關鍵字 volatile 不會發生阻塞。
  • 關鍵字 synchronized 可以保證資料的可見性和原子性,關鍵字 volatile 只能保證資料的可見性,不能保證資料的原子性。
  • 關鍵字 synchronized 解決的是多個執行緒之間訪問資源的同步性,關鍵字 volatile 解決的是變數在多個執行緒之間的可見性。

3.多執行緒相關方法

Thread 類的方法

run 和 start

方法 run 在 Runnable 介面中被定義。

方法 start 在 Thread 類中被定義。

建立一個 Thread 類的例項,即為建立了一個處於初始狀態的執行緒。對一個處於初始狀態的執行緒呼叫方法 start,該執行緒被啟動,進入執行狀態。

呼叫方法 start 之後,方法 run 會自動執行。

通過呼叫方法 start,執行方法 run,才是多執行緒工作

如果直接執行方法 run,則方法 run 會被當成一個主執行緒下的普通方法執行,而不會在某個執行緒中執行,因此不是多執行緒工作。

sleep

方法 sleepThread 類中被定義。

該方法的作用是使當前執行緒暫停執行一段時間,讓其他執行緒有機會繼續執行,但是該方法不會釋放鎖

方法 sleep 需要捕獲 InterruptedException 異常。

join

方法 join 在 Thread 類中被定義。

該方法的作用是阻塞呼叫該方法的執行緒,直到當前執行緒執行完畢之後,呼叫該方法的執行緒再繼續執行。

方法 join 需要捕獲 InterruptedException 異常。

yield

方法 yield 在 Thread 類中被定義。

該方法的作用是暫停當前正在執行的執行緒物件,並執行其他執行緒。

實際呼叫方法 yield 時無法保證一定能讓其他執行緒執行,因為執行緒排程時可能再次選中原來的執行緒物件。

Object 類的方法

wait

方法 wait 在 Object 類中被定義。

該方法必須在 synchronized 語句塊內使用,作用是釋放鎖,讓其他執行緒可以執行,當前執行緒進入等待池中。

notify 和 notifyAll

方法 notify 和 notifyAll 在 Object 類中被定義。

方法 notify 的作用是從等待池中移走任意一個等待當前物件的執行緒並放到鎖池中,只有鎖池中的執行緒可以獲取鎖。

方法 notifyAll 的作用是從等待池中移走全部等待當前物件的執行緒並放到鎖池中,鎖池中的這些執行緒將爭奪鎖。

中斷執行緒

中斷執行緒的方法是 interrupt,在 Thread 類中被定義。

該方法不會中斷一個正在執行的執行緒,只是改變中斷標記。

當執行緒處於等待狀態、超時等待狀態或阻塞狀態時,如果對執行緒呼叫方法 interrupt 將執行緒的中斷標記設為 true,則中斷標記會被清除,同時會丟擲 InterruptedException 異常。可以通過 try-catch 塊捕獲該異常,即可終止執行緒。

當執行緒處於執行狀態時,可以對執行緒呼叫方法 interrupt 將執行緒的中斷標記設為 true,從而達到終止執行緒的目的,也可以新增一個 volatile 修飾的額外標記,當需要終止執行緒時,更改該標記的值即可。

不推薦使用方法 stopdestroy 終止執行緒。

  • 方法 stop 會立即停止方法 run 中剩餘的全部工作,並丟擲 ThreadDeath 錯誤,導致清理性工作無法完成,另外方法 stop 會立即釋放該執行緒的所有鎖,導致物件狀態不一致。
  • 方法 destroy 只是丟擲 NoSuchMethodError,沒有做任何事情,因此無法終止執行緒。

方法 sleep、join 和 yield 的區別有哪些?

  • 方法 sleep 的作用是使當前執行緒暫停執行一段時間,讓其他執行緒有機會繼續執行;

  • 方法 join 的作用是阻塞呼叫該方法的執行緒,直到當前執行緒執行完畢之後,呼叫該方法的執行緒再繼續執行;

  • 方法 yield 的作用是暫停當前正在執行的執行緒物件,並執行其他執行緒。


4.執行緒池

執行緒池是一種執行緒的使用模式。

建立若干個可執行的執行緒放入一個池(容器)中,有任務需要處理時,會提交到執行緒池中的任務佇列,處理完之後執行緒並不會被銷燬,而是仍然線上程池中等待下一個任務。

執行緒池的好處

在開發過程中,合理地使用執行緒池可以帶來 3 個好處。

  • 降低資源消耗。

    重複利用執行緒池中已經建立的執行緒,可以避免頻繁地建立和銷燬執行緒,從而減少資源消耗。

  • 提高響應速度。

    由於執行緒池中有已經建立的執行緒,因此當任務到達時,可以直接執行,不需要等待執行緒建立。

  • 提高執行緒的可管理性。

    執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。

執行緒池的建立

可以通過 ThreadPoolExecutor 類建立執行緒池。ThreadPoolExecutor 類有 4 個構造方法,其中最一般化的構造方法包含 7 個引數。

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize, 
    long keepAliveTime, 
    TimeUnit unit, 
    BlockingQueue<Runnable> workQueue, 
    ThreadFactory threadFactory, 
    RejectedExecutionHandler handler
)

7 個引數的含義如下。

  • corePoolSize:核心執行緒數

    定義了最少可以同時執行的執行緒數量,當有新的任務時就會建立一個執行緒執行任務,當執行緒池中的執行緒數量達到 corePoolSize 之後,到達的任務進入阻塞佇列。

  • maximumPoolSize:最大執行緒數

    定義了執行緒池中最多能建立的執行緒數量。

  • keepAliveTime:等待時間

    當執行緒池中的執行緒數量大於 corePoolSize 時,如果一個執行緒的空閒時間達到 keepAliveTime 時則會終止,直到執行緒池中的執行緒數不超過 corePoolSize

  • unit:引數 keepAliveTime 的單位

  • workQueue:阻塞佇列

    用來儲存等待執行的任務。

  • threadFactory:建立執行緒的工廠

  • handler:當拒絕處理任務時的策略

向執行緒池提交任務

可以通過方法 execute 向執行緒池提交任務。該方法被呼叫時,執行緒池會做如下操作。

  1. 如果正在執行的執行緒數量小於 corePoolSize,則建立核心執行緒執行這個任務。
  2. 如果正在執行的執行緒數量大於或等於 corePoolSize,則將這個任務放入阻塞佇列。
  3. 如果阻塞佇列滿了,而且正在執行的執行緒數量小於 maximumPoolSize,則建立非核心執行緒執行這個任務。
  4. 如果阻塞佇列滿了,而且正在執行的執行緒數量大於或等於 maximumPoolSize,則執行緒池丟擲 RejectExecutionException 異常。

上述操作中提到了兩個概念,「核心執行緒」「非核心執行緒」。核心執行緒和非核心執行緒的最大數目在建立執行緒池時被指定。

核心執行緒和非核心執行緒的區別如下:

  • 向執行緒池提交任務時,首先建立核心執行緒執行任務,直到核心執行緒數到達上限,然後將任務放入阻塞佇列。
  • 只有在核心執行緒數到達上限,且阻塞佇列滿的情況下,才會建立非核心執行緒執行任務。

關閉執行緒池

可以通過呼叫執行緒池的方法 shutdown shutdownNow 關閉執行緒池。

這兩個方法的原理是遍歷執行緒池中的工作執行緒,對每個工作執行緒呼叫 interrupt 方法中斷執行緒,無法響應中斷的任務可能永遠無法終止。

方法 shutDown shutDownNow 有以下區別:

  • 方法 shutDown 將執行緒池的狀態設定成 SHUTDOWN,正在執行的任務繼續執行,沒有執行的任務將中斷。

  • 方法 shutDownNow 將執行緒池的狀態設定成 STOP,正在執行的任務被停止,沒有執行的任務被返回。