基礎概念
什么是線程?線程和進程的區別是什么?
線程 是程序執行的最小單位,它是 CPU 調度和執行的基本單元。一個進程可以包含多個線程,這些線程共享進程的資源(如內存),但每個線程有自己的棧和程序計數器。
線程和進程的區別:
-
定義:
-
進程 是正在執行的程序實例,它是資源分配的基本單位,擁有自己的獨立地址空間、內存、數據棧等。
-
線程 是進程中的一個執行單元,多個線程共享進程的資源(內存、文件句柄等)。
-
-
資源:
-
進程 擁有獨立的內存空間和系統資源,每個進程有自己的堆和數據段。
-
線程 在同一進程內共享內存空間,因此線程之間的通信比進程間通信更高效。
-
-
創建和銷毀:
-
進程 的創建和銷毀開銷大,因為操作系統需要為每個進程分配獨立的內存空間。
-
線程 的創建和銷毀相對輕量,開銷較小,因為線程共享同一進程的資源。
-
-
通信:
-
進程間通信(IPC) 較為復雜,通常需要通過管道、消息隊列、共享內存等方式。
-
線程間通信 相對簡單,因為同一進程中的線程共享內存,可以通過共享變量進行通信。
-
-
調度:
-
進程 的切換需要較多的系統資源,因為每個進程都有獨立的內存空間和資源。
-
線程 的切換較為輕便,調度和上下文切換的成本較低。
-
總結:進程 是資源分配的基本單位,線程是程序執行的基本單位。線程更輕量,多個線程共享進程資源,但進程之間相互獨立。
如何創建線程?有幾種方式?
創建線程有兩種主要方式:
-
繼承
Thread
類:通過繼承Thread
類并重寫run()
方法來定義線程的執行內容,最后調用start()
方法啟動線程。 -
實現
Runnable
接口:通過實現Runnable
接口的run()
方法來定義線程執行的任務,然后將Runnable
實例傳遞給Thread
類的構造函數,并調用start()
啟動線程。
這兩種方式的區別在于,繼承 Thread
類時只能繼承一個類,而實現 Runnable
接口更靈活,因為一個類可以實現多個接口,適合更復雜的應用場景。
Runnable 和 Callable 的區別?
Runnable
和 Callable
都是用于定義線程任務的接口,但它們有一些關鍵的區別:
-
返回值
-
Runnable
接口的run()
方法沒有返回值,也無法拋出異常。 -
Callable
接口的call()
方法有返回值,能夠返回一個結果或者拋出異常。
-
-
異常處理
-
Runnable
的run()
方法無法拋出任何檢查型異常(checked exceptions),只能拋出運行時異常(unchecked exceptions)。 -
Callable
的call()
方法可以拋出任何類型的異常(包括檢查型異常)。
-
-
執行方式
-
Runnable
是傳統的線程任務接口,適用于沒有返回結果或不需要處理異常的簡單任務。 -
Callable
更靈活,適用于需要返回結果或處理異常的任務,通常與ExecutorService
一起使用,可以獲取任務的返回值。
-
-
與
Future
的結合-
Runnable
任務無法直接獲取結果,因此不能與Future
結合使用來獲取任務的返回值。 -
Callable
返回值類型為V
,可以與ExecutorService.submit()
方法一起使用,返回一個Future
對象,通過Future.get()
可以獲取任務執行的結果或捕獲異常。
-
總結:如果你需要線程執行結果或可能拋出異常,使用 Callable
;如果只是需要執行某些操作而不關心返回值或異常,Runnable
更為簡單和直接。
線程的生命周期(狀態)有哪些?
線程的生命周期包括以下幾種狀態:
-
新建(New) 線程在創建后,尚未調用
start()
方法時,處于 新建狀態。此時線程對象已經創建,但尚未開始執行。 -
就緒(Runnable) 線程調用
start()
方法后,線程進入 就緒狀態,此時線程已經準備好執行,但尚未獲取到 CPU 資源。操作系統的線程調度器會根據某些算法決定哪個線程獲得 CPU 執行權。 -
運行(Running) 當線程獲得 CPU 時間片后,它進入 運行狀態,并開始執行
run()
方法中的代碼。此時線程正在執行任務。 -
阻塞(Blocked) 線程在等待某些資源(如 I/O 操作、鎖資源等)時,會進入 阻塞狀態。線程無法繼續執行,直到等待的資源可用。
-
等待(Waiting) 線程進入 等待狀態,通常是因為調用了
wait()
、join()
或park()
方法,線程會一直等待直到被其他線程喚醒(如調用notify()
、notifyAll()
、interrupt()
等)。 -
超時等待(Timed Waiting) 線程進入 超時等待狀態,當線程調用
sleep()
、join(long millis)
、wait(long millis)
等帶有超時參數的方法時,線程會在指定的時間內處于等待狀態。如果超時未被喚醒,線程會自動回到 就緒狀態。 -
終止(Terminated) 線程的
run()
方法執行完畢,或者線程因異常退出時,線程進入 終止狀態。此時線程生命周期結束,無法重新啟動。
總結:線程的生命周期涉及從新建到終止的多個狀態,線程可以在不同狀態之間切換,具體狀態由線程調度器和程序的運行條件決定。
sleep() 和 wait() 的區別?
sleep()
和 wait()
都是讓當前線程暫停執行,但它們有幾個關鍵的區別:
-
作用對象
-
sleep()
是Thread
類的方法,它使當前線程暫停執行指定的時間,當前線程仍然占有 CPU 資源。它不需要持有鎖。 -
wait()
是Object
類的方法,它使當前線程暫停執行,并且釋放對象的鎖,直到線程被其他線程喚醒。wait()
只能在同步塊或同步方法中使用。
-
-
暫停時間
-
sleep()
使線程暫停指定的時間(毫秒和納秒),一旦時間到了,線程會自動回到就緒狀態,等待操作系統重新調度。 -
wait()
會讓線程一直進入等待狀態,直到其他線程通過notify()
或notifyAll()
喚醒它,或者超時(如果指定了超時時間)。
-
-
線程的鎖狀態
-
sleep()
線程暫停時 不會釋放鎖,線程仍持有它在進入sleep()
前獲得的鎖。 -
wait()
線程暫停時 會釋放鎖,線程放棄當前對象的鎖,其他線程可以獲得該鎖,執行操作。
-
-
異常處理
-
sleep()
會拋出InterruptedException
,如果在sleep()
期間線程被中斷,會拋出此異常。 -
wait()
也會拋出InterruptedException
,如果在等待過程中線程被中斷,會拋出此異常。
-
-
常見使用場景
-
sleep()
通常用于讓線程暫停指定的時間,可以用于周期性的任務、定時器等場景。 -
wait()
通常用于線程間的通信和協作,常見于生產者-消費者模型,線程需要等待某個條件成立或等待資源釋放時使用。
-
總結:sleep()
是一個用于讓當前線程暫停執行的簡單方法,適用于線程的定時任務;而 wait()
主要用于線程間的協作與通信,常用于多線程同步中。
yield() 和 join() 的作用是什么?
yield()
和 join()
都是用于線程控制的靜態方法,但它們的作用和使用場景有所不同:
-
yield()
-
作用:
Thread.yield()
方法用于 提示線程調度器 當前線程愿意讓出 CPU 執行時間片,允許其他同優先級的線程執行。調用yield()
并不會讓當前線程停止執行,只是使當前線程回到就緒狀態,調度器會選擇其他同優先級的線程執行。 -
特點:
-
線程仍然處于就緒狀態,調用
yield()
后,線程不會立即停止,而是根據調度器的策略,可能會被調度出去,其他線程有機會執行。 -
如果沒有其他同優先級的線程準備好執行,當前線程可能繼續執行。
-
yield()
的調用效果依賴于操作系統和 JVM 的調度策略,并非一定會讓出 CPU。
-
-
常見場景:通常用于調度和協調線程的執行,特別是在多線程程序中希望讓其他線程有機會執行時。
-
-
join()
-
作用:
Thread.join()
方法使得 當前線程等待另一個線程執行完畢 后再繼續執行。即當前線程會被阻塞,直到調用join()
的線程執行完run()
方法。 -
特點:
-
如果線程 A 調用了線程 B 的
join()
方法,線程 A 會被阻塞,直到線程 B 執行完畢。 -
join()
可以指定一個超時時間,即join(long millis)
,如果超時,線程 A 會繼續執行。 -
join()
方法常用于線程的協調,確保一個線程在另一個線程執行完成之后再繼續執行。
-
-
常見場景:通常用于在多線程程序中,等待某個線程完成任務后再執行后續的操作。例如,在多個線程并行執行時,主線程等待所有子線程完成后再進行下一步操作。
-
總結:
-
yield()
是一種提示線程調度器讓當前線程暫停執行,可能會導致當前線程被暫時掛起,等待其他線程執行。 -
join()
是一種同步機制,用于確保當前線程在另一個線程執行完成后再繼續執行。
線程安全與同步
什么是線程安全?如何保證線程安全?
線程安全指的是在多線程環境下,多個線程對共享資源進行操作時,能夠保證數據的一致性和正確性,不會出現競態條件或數據錯誤。
如何保證線程安全?
-
使用同步(synchronized):通過對共享資源加鎖,確保同一時間只有一個線程可以訪問被鎖住的代碼塊或方法。
-
使用顯式鎖(如
ReentrantLock
):通過ReentrantLock
提供比synchronized
更加靈活的鎖控制,如可以中斷的鎖或定時鎖。 -
原子操作:使用
Atomic
類(如AtomicInteger
)提供原子性操作,無需顯式加鎖。 -
線程局部變量(ThreadLocal):為每個線程提供獨立的變量副本,避免共享數據。
-
不可變對象:使用不可變對象(如
String
),因為它們的狀態在創建后不能被改變,從而避免線程安全問題。
總結:確保線程安全的方式主要有同步機制、顯式鎖、原子操作、線程局部變量等,具體方法根據不同場景選擇。
synchronized 關鍵字的用法?
synchronized
的用法有三種:
-
修飾實例方法,鎖的是當前對象
this
,保證同一時刻只有一個線程執行這個方法。 -
修飾靜態方法,鎖的是當前類的
Class
對象,適用于多個線程訪問類級別的資源。 -
修飾代碼塊,鎖的是代碼塊中指定的對象,可以更細粒度地控制同步范圍,提升性能。
目的就是讓多個線程在訪問共享資源時保持互斥,避免并發問題。
synchronized 和 ReentrantLock 的區別?
synchronized
和 ReentrantLock
都可以實現線程同步,但它們有以下主要區別:
-
鎖的可重入性 兩者都是可重入鎖,線程可以多次獲得同一個鎖,不會死鎖。
-
是否可中斷
synchronized
不可中斷,線程一旦阻塞,只能等著。ReentrantLock
可以中斷,通過lockInterruptibly()
方法實現。 -
是否可超時
synchronized
無法設置超時時間。ReentrantLock
可以通過tryLock(long time)
設置等待鎖的時間,超時不再等待。 -
公平性
synchronized
是非公平的,誰先搶到誰先執行,不保證順序。ReentrantLock
可以設置為公平鎖,按線程請求順序獲取鎖。 -
鎖的釋放方式
synchronized
是自動釋放,方法或代碼塊執行完自動釋放鎖。ReentrantLock
需要手動釋放,必須調用unlock()
,否則容易造成死鎖。 -
靈活性
ReentrantLock
提供更多高級功能,如條件鎖(Condition),可實現更復雜的線程協作;synchronized
只能使用wait()
和notify()
。
總結:簡單場景用 synchronized
就夠了;需要更強控制力、響應中斷、可設置超時、需要公平性時,用 ReentrantLock
更合適。
volatile 關鍵字的作用?
volatile
是一個輕量級的同步機制,用于保證 可見性 和 禁止指令重排序。
主要作用有兩個:
-
可見性:當一個線程修改了被
volatile
修飾的變量,其他線程能立即看到這個修改。它確保了變量從主內存直接讀寫,而不是使用線程本地緩存。 -
禁止指令重排序:
volatile
會在寫操作前插入內存屏障,防止編譯器或 CPU 對指令重排,從而保證變量修改的順序性。
但注意:
-
volatile
不能保證原子性,即多個線程同時修改一個變量時不能保證正確性。比如count++
不是原子操作,用volatile
也無法保證線程安全。 -
一般用于狀態標志、雙重檢查鎖等場景。
總結:volatile
適用于保證一個變量在多線程之間的可見性,但不能代替鎖來實現復雜的同步邏輯。
什么是 CAS(Compare-And-Swap)?
CAS,全稱是 Compare-And-Swap(比較并交換),是一種原子操作,用于實現無鎖并發。
它的核心思想是:比較內存中的值是否和預期值相等,如果相等則更新為新值,否則什么都不做。
執行過程包括三個參數:
-
內存位置(變量的地址)
-
預期值(希望內存中的值是這個)
-
新值(如果相等則把它寫進去)
只有當變量的當前值等于預期值時,才會被替換成新值,否則就不做任何操作,通常會進行自旋重試。
CAS 的優點:
-
是一種樂觀鎖機制,不需要加鎖,因此效率高。
-
廣泛應用于
java.util.concurrent.atomic
包中的原子類,如AtomicInteger
。
缺點:
-
自旋開銷:高并發下可能長時間重試,浪費 CPU。
-
只能保證一個變量的原子性,多個變量操作需要配合其他手段。
-
ABA 問題:如果變量從 A 變成 B 又變回 A,CAS 無法識別這種變化。
為了解決 ABA 問題,可以使用 AtomicStampedReference
這類帶版本號的原子類。
Atomic 原子類的作用?
Atomic 原子類的作用是提供一套無鎖的線程安全操作,用于保證單個變量在并發環境下的原子性操作,避免使用 synchronized
或顯式鎖帶來的性能開銷。
這些類位于 java.util.concurrent.atomic
包中,底層依賴于 CAS(Compare-And-Swap)機制 實現。
常見的原子類包括:
-
AtomicInteger / AtomicLong / AtomicBoolean 對基本類型進行原子操作,如自增、自減、比較更新等。
-
AtomicReference 對引用類型進行原子更新,常用于實現非阻塞數據結構或共享對象更新。
-
AtomicStampedReference 解決 ABA 問題,為引用加上版本號或時間戳。
-
AtomicMarkableReference 用布爾標記解決類似 ABA 問題,更輕量。
-
AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray 用于原子地操作數組中的元素。
作用總結:
-
保證數據的可見性和原子性
-
避免加鎖,提升并發性能
-
適用于高頻簡單的并發更新操作,如計數器、狀態標志等
但注意:原子類適用于單變量的并發場景,復雜邏輯仍建議使用鎖控制。
線程池
為什么要使用線程池?
使用線程池的主要原因是為了更高效、更穩定地管理線程資源。具體來說,有以下幾點:
-
降低資源開銷 每次創建和銷毀線程是昂貴的,線程池通過復用線程,減少了頻繁創建銷毀的成本。
-
提高響應速度 任務到來時可以直接復用已有線程,無需等待新線程創建,提高響應效率。
-
統一管理線程數量 可以控制并發線程的總量,防止系統因為線程過多而耗盡資源。
-
支持任務調度與拒絕策略 線程池支持任務排隊、定時執行、周期執行,并可以設置拒絕策略應對超負荷。
-
更強的可維護性和可監控性 統一的線程管理便于日志跟蹤、異常捕獲和資源釋放,提高系統穩定性。
總結一句話:線程池是為了讓線程的使用更加高效、可控、可維護。
ThreadPoolExecutor 的核心參數有哪些?
ThreadPoolExecutor
是 Java 中最靈活的線程池實現類,它的構造函數包含幾個核心參數,決定了線程池的行為和性能。主要參數如下:
-
corePoolSize(核心線程數) 核心線程會一直保留(除非設置了 allowCoreThreadTimeOut),即使處于空閑狀態也不會被回收。線程數小于該值時,新任務會直接創建線程執行。
-
maximumPoolSize(最大線程數) 線程池能容納的最大線程數。當任務數過多,阻塞隊列滿了且核心線程數已滿,才會嘗試創建非核心線程,直到達到最大線程數。
-
keepAliveTime(空閑線程存活時間) 非核心線程在空閑狀態下,超過該時間會被回收。可以通過設置 allowCoreThreadTimeOut 為 true,讓核心線程也受到這個限制。
-
unit(時間單位) 配合 keepAliveTime 使用,指定時間的單位,例如 TimeUnit.SECONDS。
-
workQueue(任務隊列) 存放等待執行任務的隊列,有多種實現,如:
-
LinkedBlockingQueue
(無界,常用于執行大量短期任務) -
ArrayBlockingQueue
(有界,常用于控制內存) -
SynchronousQueue
(不存儲任務,適用于任務很快就能執行)
-
-
threadFactory(線程工廠) 用于創建新線程,通常用來自定義線程名字或設置為守護線程。
-
handler(拒絕策略) 當線程池已滿且隊列也滿了,任務無法處理時的策略,常見有:
-
AbortPolicy
(默認,拋出異常) -
CallerRunsPolicy
(由調用者線程執行) -
DiscardPolicy
(直接丟棄) -
DiscardOldestPolicy
(丟棄最舊的任務)
-
這些參數的組合決定了線程池的行為模型,合理配置可以提升并發性能,避免線程資源浪費或過載崩潰。
線程池的拒絕策略有哪些?
線程池的拒絕策略定義了在線程數達到 maximumPoolSize
且隊列已滿時,如何處理新提交的任務。JDK 提供了四種默認策略,都實現了 RejectedExecutionHandler
接口:
-
AbortPolicy(默認) 直接拋出
RejectedExecutionException
異常,阻止系統繼續提交任務,讓調用者知道線程池已無法處理新任務。 -
CallerRunsPolicy 由調用線程(提交任務的線程)來執行任務,不拋異常,避免任務丟失,但可能拖慢主線程速度。
-
DiscardPolicy 直接丟棄任務,不拋異常,適合對部分任務容忍丟失的場景。
-
DiscardOldestPolicy 丟棄隊列中最舊的任務,然后嘗試重新提交當前任務。如果任務提交速率過高,可能導致重要任務被踢出。
除了這四種,也可以自定義拒絕策略,實現 RejectedExecutionHandler
接口,根據業務需求編寫邏輯,比如記錄日志、發送告警等。
選擇哪種策略,需要根據業務容忍度、任務重要性和系統設計來權衡。
Executors 提供的幾種線程池?各有什么特點?
Executors
是 Java 提供的線程池工廠類,用于快速創建幾種常用線程池,雖然方便,但不推薦在生產中直接使用(理由稍后說)。它提供以下幾種線程池:
-
newFixedThreadPool(int nThreads) 創建一個固定大小的線程池,核心線程數 = 最大線程數,使用
LinkedBlockingQueue
(無界隊列)。 特點:線程數固定,適合負載穩定、線程數可控的場景。 缺點:任務過多可能導致內存撐爆(因為隊列是無界的)。 -
newCachedThreadPool() 創建一個彈性線程池,線程數幾乎不受限,空閑線程60秒回收,使用
SynchronousQueue
(不緩存任務)。 特點:適合執行大量短期異步任務。 缺點:高并發下容易創建過多線程,可能導致 OOM。 -
newSingleThreadExecutor() 創建一個單線程線程池,只有一個核心線程,使用
LinkedBlockingQueue
。 特點:所有任務串行執行,保證順序性,適合場景如日志寫入。 缺點:單線程一旦掛掉,所有任務都不能執行。 -
newScheduledThreadPool(int corePoolSize) 創建一個支持定時和周期性任務的線程池,類似定時器功能,使用
DelayedWorkQueue
。 特點:適用于定時任務、周期調度任務。 缺點:底層線程數不受限制,仍有內存風險。
為什么不推薦直接使用 Executors? 它們默認的隊列和線程數量配置不合理(如無界隊列、無限線程),在高并發下容易造成 OOM 或線程堆積。推薦自己使用 ThreadPoolExecutor
顯式設置參數,更安全、可控。阿里 Java 開發手冊也強烈建議這一點。
線程池的工作流程是怎樣的?
線程池的工作流程大致如下:
-
提交任務 線程池的工作從調用
execute()
或submit()
提交任務開始。任務被封裝成一個Runnable
或Callable
對象,然后被提交到線程池。 -
任務排隊 任務提交后,首先進入線程池的任務隊列(如
BlockingQueue
)。線程池會根據隊列的類型和大小決定任務是否能夠立即執行。常見的隊列類型有:-
無界隊列:任務一直可以加入隊列,不會拋出異常(例如
LinkedBlockingQueue
)。 -
有界隊列:當隊列滿時,任務會被拒絕(例如
ArrayBlockingQueue
)。 -
同步隊列:每個任務都會直接交給一個線程執行,不會被緩存(例如
SynchronousQueue
)。
-
-
線程池工作線程執行任務 如果線程池中有空閑線程,它們會從隊列中取出任務并執行。如果沒有空閑線程:
-
如果線程池中的線程數小于核心線程數(
corePoolSize
),線程池會創建一個新線程來執行任務。 -
如果線程池中的線程數達到核心線程數,任務將被放入隊列,等待已有線程空閑。
-
如果隊列已滿,且線程池中的線程數小于最大線程數(
maximumPoolSize
),線程池會創建新線程處理任務。 -
如果線程池中的線程數已經達到最大線程數,且隊列也已滿,線程池會根據設定的拒絕策略(如
AbortPolicy
,CallerRunsPolicy
等)處理任務。
-
-
任務完成和線程回收 當線程池中的線程執行完任務后,會進行線程回收:
-
如果是非核心線程且空閑時間超過了
keepAliveTime
,它們會被回收,線程池的線程數會減少。 -
如果線程池的大小大于
corePoolSize
,并且有空閑線程,這些線程會被銷毀,直到線程池的線程數等于核心線程數。
-
-
關閉線程池 當線程池的任務執行完畢或不再需要時,調用
shutdown()
或shutdownNow()
方法來關閉線程池。shutdown()
會等待任務執行完畢后再關閉,而shutdownNow()
會嘗試停止正在執行的任務并返回未執行的任務。
總結: 線程池的工作流程是:
-
提交任務 → 判斷線程池狀態(空閑線程/核心線程/最大線程數) → 執行任務或排隊等待 → 任務完成 → 線程回收。 線程池通過合理的資源管理和任務調度,優化了線程的創建、復用和銷毀,提升了系統的并發處理能力和性能。
并發工具類
CountDownLatch 和 CyclicBarrier 的區別?
CountDownLatch
和 CyclicBarrier
都是 Java 中常用的同步工具類,雖然它們都用于協調多個線程的執行,但它們的工作原理和應用場景有所不同。具體區別如下:
1. 功能和目的
-
CountDownLatch:用于使一個或多個線程等待其他線程完成某些操作后再執行。通過調用
countDown()
來減少計數器,計數器為零時,所有等待的線程可以繼續執行。 -
CyclicBarrier:用于使一組線程互相等待,直到所有線程都達到某個屏障點后再繼續執行。線程通過調用
await()
來等待,直到所有線程都調用await()
。
2. 計數器的可重用性
-
CountDownLatch:計數器在達到零后無法重置,一旦計數器減為零,
CountDownLatch
就會結束,不能再重新使用。適用于一次性事件的同步,例如等待所有線程完成某些初始化工作后再繼續。 -
CyclicBarrier:計數器在所有線程都到達屏障點后會自動重置,可以重復使用。適用于需要多個階段、多個線程周期性等待的場景,比如多階段的并行計算。
3. 使用場景
-
CountDownLatch:典型的場景是等待多個線程執行完某些任務后再繼續執行,如主線程等待多個工作線程的完成。
-
CyclicBarrier:典型的場景是多個線程并行執行某些任務,并在完成一個階段后等待其他線程到達同一階段,例如多個計算任務在同一屏障點等待,確保同步執行。
4. 計數器操作
-
CountDownLatch:使用
countDown()
方法來減少計數器的值,直到值為零,線程才能繼續執行。await()
方法用于等待計數器為零時的通知。 -
CyclicBarrier:使用
await()
方法使線程阻塞,直到所有線程都調用await()
,計數器達到設定值后,所有線程才會被喚醒并繼續執行。
5. 線程等待方式
-
CountDownLatch:只能等待,無法重置,所有線程必須在計數器為零后才能繼續。
-
CyclicBarrier:線程在到達屏障點后會等待,所有線程都到達后,才會繼續執行,可以進行多次等待。
總結
-
CountDownLatch 用于等待某個事件的發生,適用于一次性等待場景。
-
CyclicBarrier 用于同步多線程在某個共同點上進行等待,適用于多階段的同步場景,且具有可重用性。
Semaphore 的作用是什么?
Semaphore
是 Java 中的一個并發控制工具類,主要用于控制訪問某個資源的線程數量。它可以用來限制同時訪問某些資源的線程數,避免系統因為過多線程訪問共享資源而產生問題(如資源競爭、性能下降、線程安全問題等)。
主要作用:
-
限制資源訪問數量
Semaphore
通過維護一個計數器來控制同時訪問某個資源的線程數量。計數器的初始值代表可以同時訪問資源的最大線程數。如果有線程請求訪問資源,計數器的值減一;如果計數器值為零,新的線程就會被阻塞,直到有線程釋放資源(通過release()
方法)。 -
實現并發控制 它用于實現類似“并發限制”的場景,如限制線程池的并發線程數、數據庫連接池、限制訪問某個共享資源的線程數量等。
-
避免線程過度競爭 在某些場景下,可以用
Semaphore
限制線程訪問某些資源,避免資源過度競爭導致的性能瓶頸或者線程崩潰。
核心方法:
-
acquire():獲取許可(即請求資源)。如果沒有足夠的許可,線程會被阻塞,直到有許可可用。
-
release():釋放許可,增加計數器的值,允許其他線程訪問資源。
-
availablePermits():返回當前可用的許可數量。
-
drainPermits():返回并清空當前所有可用的許可數量。
使用場景:
-
控制并發數:例如限制數據庫連接池中最大連接數、控制請求訪問頻率等。
-
并發限流:當資源訪問過于頻繁時,可以使用
Semaphore
來限制并發數,保護資源。 -
異步任務管理:可以通過
Semaphore
來限制同時執行的異步任務數量,防止系統過載。
總結:
Semaphore
主要用于控制并發訪問的數量,可以限制對共享資源的訪問。它通過獲取和釋放許可來實現線程間的同步與資源控制。
ThreadLocal 的原理和使用場景?
原理
ThreadLocal
是一個線程局部變量類,每個線程都會有自己的變量副本,線程之間不會共享這些副本。它通過每個線程內部維護一個 ThreadLocalMap
來實現這一點,ThreadLocalMap
將 ThreadLocal
作為鍵,線程對應的變量作為值存儲。當調用 get()
或 set()
時,實際上操作的是當前線程的局部變量副本,而不是共享變量。
使用場景
-
避免共享數據的線程安全問題:當多個線程需要使用相同類型的數據時,使用
ThreadLocal
可以為每個線程提供獨立的副本,避免了線程間的共享問題和同步開銷。 -
性能優化:對于一些頻繁訪問的資源(如數據庫連接、日期格式化等),使用
ThreadLocal
可以避免多線程的同步沖突,提升性能。 -
線程獨立存儲數據:在 Web 開發中,可以用
ThreadLocal
存儲線程局部數據,比如每個請求的會話信息,避免不同請求間的數據共享。
簡而言之,ThreadLocal
用于確保每個線程都可以擁有自己獨立的存儲空間,適合處理線程獨立數據的場景。
Fork/Join 框架的作用?
Fork/Join
框架是 Java 7 引入的一個并行計算框架,旨在簡化并行任務的處理,特別是對于可以被分解成小任務并且最終結果可以合并的計算。其核心思想是分治法(Divide and Conquer),將大任務拆分為多個小任務,分別執行后再合并結果。
作用
-
并行化任務
Fork/Join
框架可以通過拆分大任務為多個小任務,并將小任務并行執行,從而有效地利用多核處理器,提高計算效率。 -
任務分解與合并 它適合用來處理那些能夠遞歸分解為小子問題的問題。每個子任務完成后,將其結果匯總或合并為最終結果。
-
任務調度優化
Fork/Join
框架通過使用工作竊取算法(Work Stealing)來優化任務調度。空閑的工作線程可以竊取其他線程未完成的任務,從而提高系統的吞吐量和資源利用率。
主要組件
-
ForkJoinPool
ForkJoinPool
是Fork/Join
框架的核心執行池,它繼承自AbstractExecutorService
。ForkJoinPool
具有工作竊取算法,可以有效管理多線程的任務執行。 -
ForkJoinTask
ForkJoinTask
是框架中任務的抽象類,包含兩個重要的子類:-
RecursiveTask:表示有返回結果的任務。
-
RecursiveAction:表示沒有返回結果的任務。
-
工作原理
-
分割任務:通過
fork()
方法將任務分解成多個子任務。 -
執行任務:通過
join()
方法等待子任務完成并返回結果。 -
合并結果:當子任務完成后,將它們的結果合并,最終生成父任務的結果。
示例應用場景
-
計算大規模的 Fibonacci 數列。
-
并行計算大規模數組的和。
-
圖像處理、搜索引擎等需要進行分治操作的計算任務。
總結
Fork/Join
框架主要用于將一個大任務拆分為多個小任務,并行執行,適合分治型計算。它通過工作竊取算法來優化多線程執行,能夠有效利用多核處理器,提高并行計算的性能。
鎖與并發優化
樂觀鎖和悲觀鎖的區別?
樂觀鎖和悲觀鎖是兩種常見的并發控制機制,用于在多線程環境下處理共享資源的訪問,防止數據不一致和競爭條件。它們的主要區別在于對鎖的使用和對并發訪問的態度不同。
1. 悲觀鎖(Pessimistic Lock)
-
概念:悲觀鎖的思想是認為在多線程環境下,數據會頻繁發生沖突,因此在訪問數據時,線程會先加鎖,其他線程只能等待,直到鎖被釋放后才能繼續執行。換句話說,悲觀鎖假設在訪問共享資源時,必然會發生沖突,因此要采取預防措施(加鎖)。
-
實現方式:通常通過
synchronized
關鍵字或者ReentrantLock
等實現。 -
特點:
-
性能開銷較大:因為每次訪問資源時都會加鎖,導致其他線程必須等待鎖釋放,可能會引起線程阻塞。
-
適用于沖突頻繁的場景:當多個線程同時操作共享資源的概率較高時,悲觀鎖可以有效防止數據不一致。
-
-
應用場景:適用于并發訪問較為激烈的環境,例如銀行轉賬、庫存更新等需要保證數據一致性的場景。
2. 樂觀鎖(Optimistic Lock)
-
概念:樂觀鎖的思想是認為在多線程環境下,數據沖突的概率較低,因此在訪問數據時,不會加鎖,而是直接操作數據。操作完成后再檢查數據是否被其他線程修改。如果數據沒有被修改,操作就成功;如果數據被修改了,樂觀鎖會重新嘗試操作。這種機制通常依賴于“版本控制”或“比較和交換”機制。
-
實現方式:常用的是CAS(Compare and Swap),通過比較值是否發生變化來判斷是否發生了并發沖突。或者通過數據庫中的版本號機制來判斷數據是否已被修改。
-
特點:
-
性能較好:因為樂觀鎖不會在每次訪問時加鎖,線程不需要阻塞,減少了鎖的開銷,性能相對較高。
-
適用于沖突較少的場景:如果數據訪問沖突的概率較低,樂觀鎖可以減少同步開銷,提升并發性能。
-
-
應用場景:適用于并發訪問較少或沖突不頻繁的環境,例如緩存更新、批量數據處理等。
3. 對比總結
特性 | 樂觀鎖 (Optimistic Lock) | 悲觀鎖 (Pessimistic Lock) |
---|---|---|
鎖的使用 | 不加鎖,操作前后進行沖突檢查 | 每次操作前都加鎖 |
性能 | 性能較高,適用于沖突較少的場景 | 性能較差,適用于沖突頻繁的場景 |
阻塞情況 | 無阻塞,只有在操作完成后檢查沖突 | 會阻塞,直到鎖被釋放才能繼續執行 |
實現方式 | CAS、版本號控制等 | synchronized 、ReentrantLock 等 |
適用場景 | 并發訪問較少或沖突不頻繁的場景 | 并發訪問較多,數據一致性要求高的場景 |
4. 總結
-
悲觀鎖適用于數據沖突頻繁的場景,通過加鎖來防止數據沖突,但可能會帶來性能損失。
-
樂觀鎖適用于數據沖突較少的場景,通過不加鎖的方式提高性能,但需要額外的沖突檢測機制(如CAS或版本控制)。
什么是死鎖?如何避免死鎖?
死鎖是什么?
死鎖(Deadlock)是指在多個線程之間發生一種特殊的情況,其中每個線程都在等待其他線程釋放它所需要的資源,但這些線程都無法繼續執行,導致程序進入一個永遠等待的狀態。簡而言之,死鎖發生時,程序中的所有線程都在相互等待資源,造成了無休止的等待,從而導致程序無法繼續執行。
死鎖的必要條件(四個條件)
死鎖通常是由以下四個條件共同作用導致的,它們被稱為死鎖的四個必要條件:
-
互斥條件(Mutual Exclusion) 至少有一個資源處于“只允許一個線程訪問”的狀態,也就是說,某個資源只能被一個線程占用。
-
請求與保持條件(Hold and Wait) 一個線程持有某些資源,同時又請求其他資源,但是請求的資源被其他線程持有,造成線程阻塞,不能繼續執行。
-
不剝奪條件(No Preemption) 已分配給線程的資源在未使用完之前不能被強行剝奪,只能在線程完成任務后才釋放資源。
-
循環等待條件(Circular Wait) 線程集合中的線程相互等待對方持有的資源,形成一個閉環。例如,線程A等待線程B的資源,線程B等待線程C的資源,線程C等待線程A的資源。
當這四個條件同時滿足時,就可能發生死鎖。
如何避免死鎖?
為了避免死鎖,可以采取以下幾種策略:
1. 避免死鎖的策略:資源請求順序
最常見的一種避免死鎖的方法是資源排序。確保所有線程按照一致的順序申請資源。通過避免循環等待,可以減少死鎖的發生。
-
策略:為所有共享資源分配一個全局順序,線程必須按照資源的順序申請資源。這樣,可以避免循環等待的條件。
例如,如果有資源A和資源B,線程必須先請求資源A,然后請求資源B,而不能反過來。
2. 使用超時機制
可以為每個線程的資源請求設置一個超時時間。如果線程在超時時間內未能獲得資源,它將放棄當前的請求,釋放已占用的資源,并嘗試重新執行或處理其他任務。
-
策略:線程在請求資源時使用帶有超時限制的
tryLock()
或在數據庫中使用SELECT ... FOR UPDATE
配合timeout
來避免死鎖。
3. 死鎖檢測
通過使用監控或日志等方式實時檢測系統中的死鎖情況,發現死鎖后采取措施,比如回滾某些操作或強制中斷某些線程。
-
策略:通過定期檢查線程間的資源請求情況,檢測是否有死鎖發生。如果發現死鎖,系統可以采取回滾、重試或中斷某些線程的操作。
4. 減少持有鎖的時間
盡量縮短線程持有鎖的時間,確保在獲取鎖時,盡量減少在鎖定資源期間執行的工作,減少死鎖的發生概率。
-
策略:線程在持有鎖時,只執行必要的工作,盡早釋放鎖。避免在持有鎖時執行耗時操作或其他可能引發阻塞的任務。
5. 使用更高層次的鎖機制
某些高級鎖機制,如 ReentrantLock
提供了內置的死鎖預防機制(如 tryLock()
)和超時處理機制,能夠更好地控制鎖的獲取與釋放,避免死鎖。
6. 鎖粒度控制
控制鎖的粒度,盡量減少每次獲取鎖的資源范圍,避免多個鎖的競爭。可以通過分層鎖、細化鎖的粒度來降低死鎖的概率。
-
策略:盡量避免鎖定多個資源,或者盡量減少鎖定資源的數量。
總結
-
死鎖 是指在多線程環境下,多個線程互相等待資源,導致程序無法繼續執行的情況。
-
避免死鎖的策略 主要包括資源請求順序、超時機制、死鎖檢測、減少鎖持有時間、使用高層鎖機制和控制鎖的粒度等方法。
-
為了有效防止死鎖,通常需要根據具體的應用場景和鎖策略做出適當的調整和優化。
AQS(AbstractQueuedSynchronizer)的作用?
AQS(AbstractQueuedSynchronizer)是 Java 并發包(java.util.concurrent
)中的一個抽象類,提供了一種基于隊列的同步器框架,用于構建自定義的同步工具(如 ReentrantLock
、CountDownLatch
、Semaphore
等)。它是實現各種同步機制的基礎框架,可以幫助開發者在并發編程中簡化鎖的實現。
AQS 的作用
AQS 提供了一個統一的抽象框架,用于實現同步器的基本操作,如請求鎖、釋放鎖、排隊等待等。它通過一個雙向隊列(FIFO 隊列)管理多個線程,確保線程按照請求的順序訪問共享資源。AQS 主要負責以下幾項工作:
-
隊列管理 AQS 使用一個隊列來管理線程的排隊。在同步操作過程中,如果某個線程無法立即獲取到鎖或資源,它會被放入隊列中,等待其他線程釋放資源后再進行獲取。AQS 管理這些線程的入隊、出隊、等待和喚醒操作。
-
線程的獲取與釋放 AQS 提供了獲取和釋放同步資源的基本操作,如
acquire()
和release()
。通過繼承 AQS 并實現其中的抽象方法,開發者可以根據自己的需求定制不同的同步器。 -
共享和獨占模式 AQS 支持兩種不同的同步模式:獨占模式和共享模式。
-
獨占模式:某個線程獲取資源后,其他線程無法獲取該資源,直到持有資源的線程釋放。
-
共享模式:多個線程可以同時獲取資源,直到資源達到上限才會阻塞等待。
-
-
線程阻塞與喚醒 AQS 管理線程的阻塞與喚醒機制。如果當前線程無法獲取到資源,它會被加入隊列并進入阻塞狀態。其他線程釋放資源后,會喚醒隊列中的線程,使其能夠繼續執行。
-
底層支持自定義同步器 通過繼承 AQS 并實現其方法,開發者可以輕松實現自定義的同步器。比如
ReentrantLock
、CountDownLatch
、Semaphore
、ReadWriteLock
等都可以通過 AQS 來實現。
AQS 的核心方法
-
acquire(int arg): 請求獲取資源,并嘗試根據給定的參數(如嘗試次數、超時等)獲得同步資源。通常用于實現鎖的獲取邏輯。
-
release(int arg): 釋放資源,表示當前線程完成任務后釋放鎖或者同步資源。通常用于實現鎖的釋放邏輯。
-
tryAcquire(int arg): 嘗試獲取資源,通常會被自定義同步器重寫,以決定是否能夠立即獲取鎖。
-
tryRelease(int arg): 嘗試釋放資源,通常會被自定義同步器重寫,執行一些資源釋放后的后處理操作。
-
acquireShared(int arg): 共享模式下請求資源。通常用于如信號量等共享資源的獲取。
-
releaseShared(int arg): 共享模式下釋放資源,通常用于信號量等資源的釋放。
AQS 的工作原理
-
隊列和線程阻塞 當一個線程請求獲取資源時,如果該資源當前不可用,線程將被加入到 AQS 的等待隊列中。線程進入等待狀態,直到有線程釋放資源,并喚醒它。
-
資源的競爭和獲取 資源的獲取通常由
tryAcquire
或tryAcquireShared
方法實現。如果這些方法成功獲取了資源,線程就可以開始執行。如果失敗,則進入隊列,等待被喚醒。 -
資源的釋放 線程完成任務后,通過
release
或releaseShared
方法釋放資源。此時,AQS 會嘗試喚醒隊列中的其他線程,讓它們有機會獲取資源。 -
隊列的管理 AQS 會確保線程按照請求的順序進行排隊等待,FIFO 順序。如果線程獲取資源成功,它就會從隊列中移除,繼續執行。
AQS 的應用
AQS 本身并不會直接用于同步操作,而是作為一個底層工具,幫助開發者構建自定義的同步工具。基于 AQS,Java 提供了很多常見的同步工具,例如:
-
ReentrantLock:可重入鎖,實現了獨占鎖的功能。
-
CountDownLatch:允許一個或多個線程等待其他線程執行完成后再繼續執行。
-
Semaphore:限制某個資源的最大并發線程數,控制訪問資源的線程數。
-
ReadWriteLock:讀寫鎖,允許多個線程同時讀取,但在寫操作時獨占資源。
-
CyclicBarrier:讓一組線程在某個階段等待,直到所有線程都達到這個階段。
總結
AQS 是 Java 并發包中的一個核心類,用于構建自定義同步器。它通過一個隊列管理線程的排隊,支持獨占模式和共享模式,提供線程獲取和釋放資源的基本操作,幫助開發者簡化復雜的同步控制。通過 AQS,開發者可以輕松實現高效的并發控制機制,如鎖、信號量、計數器等同步工具。
什么是偏向鎖、輕量級鎖、重量級鎖?
偏向鎖、輕量級鎖和重量級鎖是 Java 虛擬機(JVM)中針對線程競爭的不同優化策略。它們的設計目的是為了減少鎖競爭和提高性能。它們在 JDK 的不同版本中不斷改進,尤其是隨著 JDK 1.6 和之后版本的引入,使得鎖的性能得到了顯著提升。下面是它們的具體解釋和區別。
1. 偏向鎖(Biased Locking)
偏向鎖是 JVM 為了優化單線程場景下的鎖競爭而提出的一種鎖優化策略。其目的是減少獲取鎖的開銷,特別是在只有一個線程訪問同步塊時。
-
偏向鎖的工作原理: 偏向鎖的基本思想是當一個線程第一次獲得鎖時,會將鎖的標記記錄在對象頭中,并且在后續獲取鎖時,不需要做任何的同步操作,直接獲取對象的鎖。只有當其他線程競爭該鎖時,才會撤銷偏向鎖,轉為輕量級鎖或重量級鎖。
-
何時使用: 偏向鎖適用于絕大多數情況下只有一個線程訪問某個對象的場景,比如緩存操作、日志記錄等。它減少了每次獲取鎖時的性能開銷。
-
撤銷條件: 偏向鎖會在以下情況下被撤銷:
-
另一個線程嘗試獲取該鎖。
-
當前線程被中斷。
-
當前線程在獲取鎖時發生了死鎖。
-
2. 輕量級鎖(Lightweight Locking)
輕量級鎖是為了解決偏向鎖撤銷后的鎖競爭問題而提出的優化機制。它的目標是提高鎖的性能,避免每次加鎖都進入重量級鎖的狀態。
-
輕量級鎖的工作原理: 輕量級鎖的基本思路是線程在獲取鎖時,會先嘗試使用一個稱為鎖標記(Lock Record)的結構來判斷是否已經有線程持有該鎖。這個過程是無鎖的,只有在發生鎖競爭時,才會轉為重量級鎖。輕量級鎖的實現依賴于 CAS(Compare-And-Swap) 操作,如果 CAS 成功,則鎖定成功;否則,如果鎖被其他線程占用,則會撤銷輕量級鎖并進入阻塞狀態(進入重量級鎖)。
-
何時使用: 適用于少量線程競爭的場景。如果只有一個線程訪問共享資源,輕量級鎖能夠提供較好的性能。如果有多個線程競爭,輕量級鎖會變成重量級鎖,導致性能下降。
-
特點:
-
輕量級鎖不需要進行系統調用,盡量避免了進入阻塞隊列。
-
線程只有在競爭時才會升級為重量級鎖,從而減少了不必要的鎖競爭。
-
3. 重量級鎖(Heavyweight Locking)
重量級鎖是傳統的鎖機制,它通常是指 synchronized
鎖。當鎖競爭較激烈時,JVM 會將輕量級鎖升級為重量級鎖。重量級鎖會導致線程阻塞和喚醒,通常會引入系統調用,影響性能。
-
重量級鎖的工作原理: 當多個線程爭用同一個鎖時,JVM 會使用操作系統的互斥機制(例如互斥量或信號量)來保護共享資源。當一個線程獲取不到鎖時,它會被掛起并進入阻塞狀態,直到其他線程釋放鎖為止。
-
何時使用: 在高并發情況下,當多個線程爭用同一個鎖時,輕量級鎖無法解決問題,鎖會被升級為重量級鎖。重量級鎖會嚴重影響性能,導致線程上下文切換和阻塞。
-
特點:
-
在多線程競爭激烈時,重量級鎖會涉及線程的上下文切換和內核調度,性能開銷較大。
-
進入重量級鎖后,線程會被阻塞,直到鎖釋放。
-
4. 總結
鎖類型 | 描述 | 適用場景 | 性能表現 |
---|---|---|---|
偏向鎖 | 優化單線程場景,當只有一個線程競爭時不會進行加鎖操作。 | 適用于只有一個線程操作的場景,線程競爭少。 | 性能最好,幾乎沒有開銷。 |
輕量級鎖 | 當多個線程競爭時,采用 CAS 等機制減少鎖的開銷,避免進入阻塞狀態。 | 線程競爭較少的情況。 | 性能優于重量級鎖,但在競爭時會升級為重量級鎖。 |
重量級鎖 | 傳統的鎖機制,線程會進入阻塞狀態,通過操作系統的機制實現同步。 | 線程競爭激烈的情況。 | 性能差,涉及阻塞、喚醒、上下文切換等開銷。 |
5. JVM 實現鎖優化
JVM 會根據運行時的不同情況動態調整鎖的狀態:
-
初始時可能是偏向鎖,適應單線程環境。
-
如果有多個線程競爭,偏向鎖會升級為輕量級鎖。
-
當輕量級鎖無法滿足并發需求時,鎖會被升級為重量級鎖。
這種鎖的升級機制是為了盡可能減少鎖的開銷,提供最佳的性能。因此,JVM 的鎖優化是動態的,并且會根據線程競爭的情況做出相應的調整。
什么是自旋鎖?
自旋鎖是一種同步機制,它通過不斷循環檢查某個條件(例如鎖的狀態)來獲取鎖,而不是讓線程進入阻塞狀態。線程在獲取鎖時,如果發現鎖已被其他線程占用,它不會立即放棄,而是會持續檢查鎖是否釋放,直到獲得鎖為止。
自旋鎖的特點:
-
無阻塞:自旋鎖不會讓線程進入阻塞狀態,它讓線程持續檢查鎖的狀態。這樣避免了線程阻塞和喚醒的開銷,減少了上下文切換。
-
適用于鎖持有時間短的場景:當鎖的持有時間很短時,自旋鎖比阻塞式鎖(如
synchronized
)更高效,因為它避免了線程的上下文切換。 -
CPU消耗:在高并發情況下,線程會一直自旋等待鎖,導致占用大量 CPU 資源。如果鎖的持有時間過長,CPU 會被浪費掉。
-
沒有公平性保證:自旋鎖不能保證先到的線程優先獲取鎖,可能會導致某些線程一直無法獲取到鎖。
自旋鎖的應用場景:
-
鎖持有時間短的場景:自旋鎖適合用于鎖持有時間非常短的場景,例如一個線程執行的操作只需要幾次 CPU 時鐘周期就能完成。
-
高并發的場景:在某些高并發場景下,如果大部分時間只有一個線程能成功獲取鎖,其他線程可以通過自旋快速等待,避免了不必要的上下文切換。
-
避免線程阻塞:自旋鎖能避免線程被掛起,特別適用于鎖競爭較小的場景。
自旋鎖的缺點:
-
CPU占用高:如果鎖的持有時間較長,線程將會長時間自旋,占用大量 CPU 資源,降低系統的性能。
-
鎖競爭激烈時不適用:當多個線程頻繁競爭鎖時,自旋鎖可能會導致嚴重的性能問題,因為線程會一直消耗 CPU 資源。
-
沒有公平性:自旋鎖不保證鎖的獲取順序,因此可能會出現某些線程長時間無法獲得鎖的情況。
總的來說,自旋鎖在某些場景下能提高性能,但在鎖競爭激烈或鎖持有時間長的情況下,它的缺點會非常明顯,因此使用時需要謹慎。
JMM(Java 內存模型)
什么是 JMM?
JMM(Java Memory Model,Java內存模型)是 Java 程序中線程間通信和共享數據的規范,它定義了 Java 程序中不同線程如何訪問共享變量、如何同步變量的值,以及如何確保多線程程序的正確執行。JMM 主要目的是確保 Java 程序的可見性、原子性和有序性,以便在多線程環境下避免出現不可預料的行為。
JMM的核心概念
-
內存共享: 在 Java 中,所有線程共享一塊內存區域。每個線程有自己的工作內存,線程的工作內存存儲了它所使用的變量副本,而共享變量存儲在主內存中。線程對共享變量的讀寫操作必須通過主內存來進行。
-
主內存和工作內存:
-
主內存:存儲所有線程共享的變量,線程通過主內存進行讀寫操作。
-
工作內存:每個線程有自己的工作內存,工作內存是線程對變量的本地副本,線程在工作內存中操作共享變量。
-
-
JMM的目標: JMM 的設計目標是確保多線程編程中共享變量的可見性、原子性和有序性:
-
可見性:當一個線程修改了共享變量的值,其他線程能及時看到這個修改。
-
原子性:一個操作要么全部執行成功,要么完全不執行,不會受到其他線程的干擾。
-
有序性:程序中代碼的執行順序必須符合語義,避免由于指令重排序導致的異常行為。
-
JMM的關鍵規則
-
主內存與工作內存的交互規則:
-
讀取共享變量:當線程要讀取共享變量時,必須通過工作內存讀取主內存中的變量。
-
寫入共享變量:當線程要修改共享變量時,必須將修改的值寫入主內存。
-
-
volatile變量:
-
使用
volatile
關鍵字修飾的變量,能夠保證線程對該變量的修改對其他線程是可見的。即當一個線程修改了volatile
變量的值,其他線程立即能夠看到該變化。 -
但是,
volatile
并不能保證復合操作的原子性,像++
這種操作仍然會出現線程安全問題。
-
-
Happens-Before原則: JMM定義了happens-before原則,用于確定線程間的操作順序。它描述了不同線程之間的操作是否能夠保證順序執行。
-
程序順序規則:一個線程內的操作按照程序的順序執行。
-
鎖規則:在進入某個鎖(如
synchronized
)之前的操作,happens-before 鎖釋放之后的操作。 -
volatile規則:對
volatile
變量的寫操作 happens-before 任何后續對該變量的讀操作。
-
-
重排序和指令重排: JMM允許一定程度的指令重排序,以提高性能。但這可能會導致程序執行結果與預期不一致。為了避免不必要的重排序,JMM通過同步機制(如
synchronized
、volatile
)來保證線程之間的正確執行順序。
JMM的內存可見性問題
在多線程環境中,由于每個線程有自己的工作內存,線程對共享變量的修改在某些情況下可能不會及時傳遞到其他線程。例如:
-
線程1修改共享變量A的值,但線程2未能及時讀取到線程1修改后的最新值。這種情況稱為內存可見性問題。
解決這個問題的一些方法包括:
-
使用
volatile
關鍵字,確保修改立即對其他線程可見。 -
使用
synchronized
塊,確保對共享變量的訪問是同步的,避免內存可見性問題。
JMM的原子性和有序性
-
原子性:JMM確保一些基本操作(如讀取、寫入)是原子的,但像
++
這樣的復合操作不是原子的,需要通過同步機制(例如synchronized
或Atomic
類)來保證原子性。 -
有序性:JMM通過允許一定程度的指令重排來提高性能。但為了避免重排序導致的錯誤,需要使用同步機制來確保正確的執行順序。比如
synchronized
和volatile
可以確保有序性。
總結
JMM定義了 Java 程序中多線程如何正確地共享數據,它通過規定內存模型的規則,保證了線程間的可見性、原子性和有序性。理解 JMM 可以幫助我們更好地設計多線程程序,避免常見的并發問題。
happens-before 原則是什么?
Happens-Before原則是Java內存模型(JMM)中定義的線程操作順序的規則,它保證了多線程環境下線程之間的操作順序及可見性。它的基本意思是:一個操作必須發生在另一個操作之前,從而確保前一個操作的結果能被后續操作看到。
Happens-Before的核心規則
-
程序順序規則:一個線程內的操作總是按照程序順序執行的,也就是說,前面的操作 happens-before 后面的操作。
-
鎖規則:在多個線程之間,如果一個線程釋放了鎖,那么另一個線程在獲取該鎖時,釋放鎖的操作 happens-before 獲取鎖的操作。這樣,第二個線程能夠看到第一個線程對共享變量的修改。
-
volatile規則:對
volatile
變量的寫操作 happens-before 任何后續對該變量的讀操作。即當一個線程修改了volatile
變量的值,其他線程立刻可以看到修改后的值。 -
線程啟動規則:當一個線程調用另一個線程的
start()
方法時,start()
操作 happens-before 被啟動線程的任何其他操作,確保啟動線程的狀態是可見的。 -
線程結束規則:當一個線程調用
join()
方法等待另一個線程結束時,join()
操作 happens-before 被調用線程的結束操作。確保前一個線程的執行完成,后續的線程才能繼續。 -
中斷規則:線程的中斷操作 happens-before
isInterrupted()
檢查。
為什么Happens-Before很重要?
Happens-Before原則定義了線程之間的操作順序,確保了在多線程程序中,線程間的共享數據的一致性和可見性。它幫助開發者理解在不同線程之間如何正確地同步數據,從而避免線程安全問題。
什么是內存可見性問題?如何解決?
內存可見性問題是指在多線程環境下,一個線程對共享變量的修改,其他線程可能無法立即看到該修改的情況。這種問題會導致線程間的同步失效,造成程序的行為不可預測。
內存可見性問題的原因:
-
線程本地緩存:現代處理器為提高性能,會對線程的工作內存進行優化,每個線程都有自己的一塊工作內存,線程對共享變量的修改可能只會影響到自己本地的緩存,而不會立刻同步到主內存中,導致其他線程無法看到這個修改。
-
CPU重排序:為了提高執行效率,CPU可能會對指令進行重排序,這可能會改變程序中操作的執行順序,導致一個線程對共享變量的修改在另一個線程訪問之前不可見。
-
不適當的同步:如果多個線程對共享數據進行操作時沒有適當的同步機制,也會導致線程間的共享數據不一致,無法保證修改的可見性。
如何解決內存可見性問題?
-
使用
volatile
關鍵字:volatile
關鍵字保證了對該變量的修改對所有線程是可見的。即每次讀取volatile
變量時,都會直接從主內存中讀取最新的值,而不是從線程的工作內存中讀取。這就確保了多個線程間對該變量的修改能夠及時被其他線程看到。 -
使用
synchronized
關鍵字:synchronized
關鍵字通過加鎖來確保共享數據的可見性和原子性。每個線程在進入一個同步塊時,必須首先獲取鎖,并在執行完同步塊后釋放鎖。這樣,線程之間的共享變量修改會通過鎖的機制同步到主內存,從而保證數據的可見性。 -
使用
java.util.concurrent
包中的工具類:-
Atomic
類:例如AtomicInteger
,AtomicBoolean
等類提供了原子性操作,保證線程安全,并且它們的操作通常是直接通過硬件提供的原子操作實現的。 -
CountDownLatch
、CyclicBarrier
等同步工具:這些工具類能夠幫助線程之間協調執行順序,從而保證共享變量在不同線程之間的可見性。
-
-
使用
final
關鍵字: 對于一些不可變的對象,使用final
關鍵字可以保證線程安全。當一個對象被標記為final
時,JVM 會保證該對象的初始化操作是線程安全的,也就是說,final
字段一旦賦值,其他線程就能夠立即看到該字段的值。 -
使用內存屏障(Memory Barrier): 內存屏障是硬件或操作系統提供的底層機制,用來防止指令重排序,確保操作的順序性。雖然這種機制通常由 JVM 和底層硬件處理,但我們可以通過使用高層次的同步機制(如
volatile
和synchronized
)來間接控制內存屏障的使用。
總結
內存可見性問題會導致多線程程序出現不可預知的錯誤。為了解決這個問題,我們可以通過使用 volatile
、synchronized
等同步機制,確保線程間對共享變量的修改能夠及時且正確地傳播到其他線程,從而保證程序的一致性和可靠性。
final 關鍵字在多線程中的作用?
抱歉,下面我不會再提供代碼,只會解釋概念。
在多線程中,final
關鍵字的作用主要體現在以下幾個方面:
1. 保證對象的安全發布
在多線程環境下,當多個線程共享同一個對象時,確保對象在構造完成后能夠正確地被其他線程看到是至關重要的。final
關鍵字能夠保證一個對象在構造完成后,其值對其他線程是可見的。這是因為 final
修飾的字段保證了在對象構造過程中,不會出現字段的重排序或值的延遲寫入,使得構造完成后的對象狀態對其他線程是一致的。
2. 創建不可變對象
final
可以用于字段,確保字段在對象構造后不可修改。通過 final
,可以創建不可變對象(Immutable Object)。不可變對象的狀態在構造完成后不能被改變,因此在多線程環境下,不需要擔心多個線程同時修改對象的狀態,會造成數據不一致的問題。這種特性使得不可變對象天然具有線程安全性。
3. 防止重排序
final
關鍵字還與JVM的內存模型緊密相關。它能確保變量在構造時的初始化順序,防止指令重排序(即,JVM或CPU為了優化性能而調整指令執行順序的行為)。這樣,可以保證在構造過程中,final
變量的值在對象構造完成之前不會發生變化,確保其他線程能夠正確地看到該變量的值。
總結
final
關鍵字在多線程中的作用主要是:
-
保證對象構造完成后,其字段在所有線程中都是可見的;
-
使得對象不可修改,從而避免多個線程修改對象狀態造成的競爭條件;
-
防止指令重排序,確保變量初始化順序,避免可見性問題。
這些特點使得 final
成為實現線程安全和正確發布共享變量的重要工具。