面試篇:Java并發與多線程

基礎概念

什么是線程?線程和進程的區別是什么?

線程 是程序執行的最小單位,它是 CPU 調度和執行的基本單元。一個進程可以包含多個線程,這些線程共享進程的資源(如內存),但每個線程有自己的棧和程序計數器。

線程和進程的區別

  1. 定義

    • 進程 是正在執行的程序實例,它是資源分配的基本單位,擁有自己的獨立地址空間、內存、數據棧等。

    • 線程 是進程中的一個執行單元,多個線程共享進程的資源(內存、文件句柄等)。

  2. 資源

    • 進程 擁有獨立的內存空間和系統資源,每個進程有自己的堆和數據段。

    • 線程 在同一進程內共享內存空間,因此線程之間的通信比進程間通信更高效。

  3. 創建和銷毀

    • 進程 的創建和銷毀開銷大,因為操作系統需要為每個進程分配獨立的內存空間。

    • 線程 的創建和銷毀相對輕量,開銷較小,因為線程共享同一進程的資源。

  4. 通信

    • 進程間通信(IPC) 較為復雜,通常需要通過管道、消息隊列、共享內存等方式。

    • 線程間通信 相對簡單,因為同一進程中的線程共享內存,可以通過共享變量進行通信。

  5. 調度

    • 進程 的切換需要較多的系統資源,因為每個進程都有獨立的內存空間和資源。

    • 線程 的切換較為輕便,調度和上下文切換的成本較低。

總結:進程 是資源分配的基本單位,線程是程序執行的基本單位。線程更輕量,多個線程共享進程資源,但進程之間相互獨立。

如何創建線程?有幾種方式?

創建線程有兩種主要方式:

  1. 繼承 Thread 類:通過繼承 Thread 類并重寫 run() 方法來定義線程的執行內容,最后調用 start() 方法啟動線程。

  2. 實現 Runnable 接口:通過實現 Runnable 接口的 run() 方法來定義線程執行的任務,然后將 Runnable 實例傳遞給 Thread 類的構造函數,并調用 start() 啟動線程。

這兩種方式的區別在于,繼承 Thread 類時只能繼承一個類,而實現 Runnable 接口更靈活,因為一個類可以實現多個接口,適合更復雜的應用場景。

Runnable 和 Callable 的區別?

RunnableCallable 都是用于定義線程任務的接口,但它們有一些關鍵的區別:

  1. 返回值

    • Runnable 接口的 run() 方法沒有返回值,也無法拋出異常。

    • Callable 接口的 call() 方法有返回值,能夠返回一個結果或者拋出異常。

  2. 異常處理

    • Runnablerun() 方法無法拋出任何檢查型異常(checked exceptions),只能拋出運行時異常(unchecked exceptions)。

    • Callablecall() 方法可以拋出任何類型的異常(包括檢查型異常)。

  3. 執行方式

    • Runnable 是傳統的線程任務接口,適用于沒有返回結果或不需要處理異常的簡單任務。

    • Callable 更靈活,適用于需要返回結果或處理異常的任務,通常與 ExecutorService 一起使用,可以獲取任務的返回值。

  4. Future 的結合

    • Runnable 任務無法直接獲取結果,因此不能與 Future 結合使用來獲取任務的返回值。

    • Callable 返回值類型為 V,可以與 ExecutorService.submit() 方法一起使用,返回一個 Future 對象,通過 Future.get() 可以獲取任務執行的結果或捕獲異常。

總結:如果你需要線程執行結果或可能拋出異常,使用 Callable;如果只是需要執行某些操作而不關心返回值或異常,Runnable 更為簡單和直接。

線程的生命周期(狀態)有哪些?

線程的生命周期包括以下幾種狀態:

  1. 新建(New) 線程在創建后,尚未調用 start() 方法時,處于 新建狀態。此時線程對象已經創建,但尚未開始執行。

  2. 就緒(Runnable) 線程調用 start() 方法后,線程進入 就緒狀態,此時線程已經準備好執行,但尚未獲取到 CPU 資源。操作系統的線程調度器會根據某些算法決定哪個線程獲得 CPU 執行權。

  3. 運行(Running) 當線程獲得 CPU 時間片后,它進入 運行狀態,并開始執行 run() 方法中的代碼。此時線程正在執行任務。

  4. 阻塞(Blocked) 線程在等待某些資源(如 I/O 操作、鎖資源等)時,會進入 阻塞狀態。線程無法繼續執行,直到等待的資源可用。

  5. 等待(Waiting) 線程進入 等待狀態,通常是因為調用了 wait()join()park() 方法,線程會一直等待直到被其他線程喚醒(如調用 notify()notifyAll()interrupt() 等)。

  6. 超時等待(Timed Waiting) 線程進入 超時等待狀態,當線程調用 sleep()join(long millis)wait(long millis) 等帶有超時參數的方法時,線程會在指定的時間內處于等待狀態。如果超時未被喚醒,線程會自動回到 就緒狀態

  7. 終止(Terminated) 線程的 run() 方法執行完畢,或者線程因異常退出時,線程進入 終止狀態。此時線程生命周期結束,無法重新啟動。

總結:線程的生命周期涉及從新建到終止的多個狀態,線程可以在不同狀態之間切換,具體狀態由線程調度器和程序的運行條件決定。

sleep() 和 wait() 的區別?

sleep()wait() 都是讓當前線程暫停執行,但它們有幾個關鍵的區別:

  1. 作用對象

    • sleep()Thread 類的方法,它使當前線程暫停執行指定的時間,當前線程仍然占有 CPU 資源。它不需要持有鎖。

    • wait()Object 類的方法,它使當前線程暫停執行,并且釋放對象的鎖,直到線程被其他線程喚醒。wait() 只能在同步塊或同步方法中使用。

  2. 暫停時間

    • sleep() 使線程暫停指定的時間(毫秒和納秒),一旦時間到了,線程會自動回到就緒狀態,等待操作系統重新調度。

    • wait() 會讓線程一直進入等待狀態,直到其他線程通過 notify()notifyAll() 喚醒它,或者超時(如果指定了超時時間)。

  3. 線程的鎖狀態

    • sleep() 線程暫停時 不會釋放鎖,線程仍持有它在進入 sleep() 前獲得的鎖。

    • wait() 線程暫停時 會釋放鎖,線程放棄當前對象的鎖,其他線程可以獲得該鎖,執行操作。

  4. 異常處理

    • sleep() 會拋出 InterruptedException,如果在 sleep() 期間線程被中斷,會拋出此異常。

    • wait() 也會拋出 InterruptedException,如果在等待過程中線程被中斷,會拋出此異常。

  5. 常見使用場景

    • sleep() 通常用于讓線程暫停指定的時間,可以用于周期性的任務、定時器等場景。

    • wait() 通常用于線程間的通信和協作,常見于生產者-消費者模型,線程需要等待某個條件成立或等待資源釋放時使用。

總結:sleep() 是一個用于讓當前線程暫停執行的簡單方法,適用于線程的定時任務;而 wait() 主要用于線程間的協作與通信,常用于多線程同步中。

yield() 和 join() 的作用是什么?

yield()join() 都是用于線程控制的靜態方法,但它們的作用和使用場景有所不同:

  1. yield()

    • 作用Thread.yield() 方法用于 提示線程調度器 當前線程愿意讓出 CPU 執行時間片,允許其他同優先級的線程執行。調用 yield() 并不會讓當前線程停止執行,只是使當前線程回到就緒狀態,調度器會選擇其他同優先級的線程執行。

    • 特點

      • 線程仍然處于就緒狀態,調用 yield() 后,線程不會立即停止,而是根據調度器的策略,可能會被調度出去,其他線程有機會執行。

      • 如果沒有其他同優先級的線程準備好執行,當前線程可能繼續執行。

      • yield() 的調用效果依賴于操作系統和 JVM 的調度策略,并非一定會讓出 CPU。

    • 常見場景:通常用于調度和協調線程的執行,特別是在多線程程序中希望讓其他線程有機會執行時。

  2. join()

    • 作用Thread.join() 方法使得 當前線程等待另一個線程執行完畢 后再繼續執行。即當前線程會被阻塞,直到調用 join() 的線程執行完 run() 方法。

    • 特點

      • 如果線程 A 調用了線程 B 的 join() 方法,線程 A 會被阻塞,直到線程 B 執行完畢。

      • join() 可以指定一個超時時間,即 join(long millis),如果超時,線程 A 會繼續執行。

      • join() 方法常用于線程的協調,確保一個線程在另一個線程執行完成之后再繼續執行。

    • 常見場景:通常用于在多線程程序中,等待某個線程完成任務后再執行后續的操作。例如,在多個線程并行執行時,主線程等待所有子線程完成后再進行下一步操作。

總結:

  • yield() 是一種提示線程調度器讓當前線程暫停執行,可能會導致當前線程被暫時掛起,等待其他線程執行。

  • join() 是一種同步機制,用于確保當前線程在另一個線程執行完成后再繼續執行。

線程安全與同步

什么是線程安全?如何保證線程安全?

線程安全指的是在多線程環境下,多個線程對共享資源進行操作時,能夠保證數據的一致性和正確性,不會出現競態條件或數據錯誤。

如何保證線程安全?

  1. 使用同步(synchronized):通過對共享資源加鎖,確保同一時間只有一個線程可以訪問被鎖住的代碼塊或方法。

  2. 使用顯式鎖(如 ReentrantLock:通過 ReentrantLock 提供比 synchronized 更加靈活的鎖控制,如可以中斷的鎖或定時鎖。

  3. 原子操作:使用 Atomic 類(如 AtomicInteger)提供原子性操作,無需顯式加鎖。

  4. 線程局部變量(ThreadLocal):為每個線程提供獨立的變量副本,避免共享數據。

  5. 不可變對象:使用不可變對象(如 String),因為它們的狀態在創建后不能被改變,從而避免線程安全問題。

總結:確保線程安全的方式主要有同步機制、顯式鎖、原子操作、線程局部變量等,具體方法根據不同場景選擇。

synchronized 關鍵字的用法?

synchronized 的用法有三種:

  1. 修飾實例方法,鎖的是當前對象 this,保證同一時刻只有一個線程執行這個方法。

  2. 修飾靜態方法,鎖的是當前類的 Class 對象,適用于多個線程訪問類級別的資源。

  3. 修飾代碼塊,鎖的是代碼塊中指定的對象,可以更細粒度地控制同步范圍,提升性能。

目的就是讓多個線程在訪問共享資源時保持互斥,避免并發問題。

synchronized 和 ReentrantLock 的區別?

synchronizedReentrantLock 都可以實現線程同步,但它們有以下主要區別:

  1. 鎖的可重入性 兩者都是可重入鎖,線程可以多次獲得同一個鎖,不會死鎖。

  2. 是否可中斷 synchronized 不可中斷,線程一旦阻塞,只能等著。 ReentrantLock 可以中斷,通過 lockInterruptibly() 方法實現。

  3. 是否可超時 synchronized 無法設置超時時間。 ReentrantLock 可以通過 tryLock(long time) 設置等待鎖的時間,超時不再等待。

  4. 公平性 synchronized 是非公平的,誰先搶到誰先執行,不保證順序。 ReentrantLock 可以設置為公平鎖,按線程請求順序獲取鎖。

  5. 鎖的釋放方式 synchronized 是自動釋放,方法或代碼塊執行完自動釋放鎖。 ReentrantLock 需要手動釋放,必須調用 unlock(),否則容易造成死鎖。

  6. 靈活性 ReentrantLock 提供更多高級功能,如條件鎖(Condition),可實現更復雜的線程協作;synchronized 只能使用 wait()notify()

總結:簡單場景用 synchronized 就夠了;需要更強控制力、響應中斷、可設置超時、需要公平性時,用 ReentrantLock 更合適。

volatile 關鍵字的作用?

volatile 是一個輕量級的同步機制,用于保證 可見性禁止指令重排序

主要作用有兩個:

  1. 可見性:當一個線程修改了被 volatile 修飾的變量,其他線程能立即看到這個修改。它確保了變量從主內存直接讀寫,而不是使用線程本地緩存。

  2. 禁止指令重排序volatile 會在寫操作前插入內存屏障,防止編譯器或 CPU 對指令重排,從而保證變量修改的順序性。

但注意:

  • volatile 不能保證原子性,即多個線程同時修改一個變量時不能保證正確性。比如 count++ 不是原子操作,用 volatile 也無法保證線程安全。

  • 一般用于狀態標志、雙重檢查鎖等場景。

總結:volatile 適用于保證一個變量在多線程之間的可見性,但不能代替鎖來實現復雜的同步邏輯。

什么是 CAS(Compare-And-Swap)?

CAS,全稱是 Compare-And-Swap(比較并交換),是一種原子操作,用于實現無鎖并發。

它的核心思想是:比較內存中的值是否和預期值相等,如果相等則更新為新值,否則什么都不做。

執行過程包括三個參數:

  • 內存位置(變量的地址)

  • 預期值(希望內存中的值是這個)

  • 新值(如果相等則把它寫進去)

只有當變量的當前值等于預期值時,才會被替換成新值,否則就不做任何操作,通常會進行自旋重試。

CAS 的優點:

  • 是一種樂觀鎖機制,不需要加鎖,因此效率高。

  • 廣泛應用于 java.util.concurrent.atomic 包中的原子類,如 AtomicInteger

缺點:

  1. 自旋開銷:高并發下可能長時間重試,浪費 CPU。

  2. 只能保證一個變量的原子性,多個變量操作需要配合其他手段。

  3. ABA 問題:如果變量從 A 變成 B 又變回 A,CAS 無法識別這種變化。

為了解決 ABA 問題,可以使用 AtomicStampedReference 這類帶版本號的原子類。

Atomic 原子類的作用?

Atomic 原子類的作用是提供一套無鎖的線程安全操作,用于保證單個變量在并發環境下的原子性操作,避免使用 synchronized 或顯式鎖帶來的性能開銷。

這些類位于 java.util.concurrent.atomic 包中,底層依賴于 CAS(Compare-And-Swap)機制 實現。

常見的原子類包括:

  1. AtomicInteger / AtomicLong / AtomicBoolean 對基本類型進行原子操作,如自增、自減、比較更新等。

  2. AtomicReference 對引用類型進行原子更新,常用于實現非阻塞數據結構或共享對象更新。

  3. AtomicStampedReference 解決 ABA 問題,為引用加上版本號或時間戳。

  4. AtomicMarkableReference 用布爾標記解決類似 ABA 問題,更輕量。

  5. AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray 用于原子地操作數組中的元素。

作用總結:

  • 保證數據的可見性和原子性

  • 避免加鎖,提升并發性能

  • 適用于高頻簡單的并發更新操作,如計數器、狀態標志等

但注意:原子類適用于單變量的并發場景,復雜邏輯仍建議使用鎖控制。

線程池

為什么要使用線程池?

使用線程池的主要原因是為了更高效、更穩定地管理線程資源。具體來說,有以下幾點:

  1. 降低資源開銷 每次創建和銷毀線程是昂貴的,線程池通過復用線程,減少了頻繁創建銷毀的成本。

  2. 提高響應速度 任務到來時可以直接復用已有線程,無需等待新線程創建,提高響應效率。

  3. 統一管理線程數量 可以控制并發線程的總量,防止系統因為線程過多而耗盡資源。

  4. 支持任務調度與拒絕策略 線程池支持任務排隊、定時執行、周期執行,并可以設置拒絕策略應對超負荷。

  5. 更強的可維護性和可監控性 統一的線程管理便于日志跟蹤、異常捕獲和資源釋放,提高系統穩定性。

總結一句話:線程池是為了讓線程的使用更加高效、可控、可維護。

ThreadPoolExecutor 的核心參數有哪些?

ThreadPoolExecutor 是 Java 中最靈活的線程池實現類,它的構造函數包含幾個核心參數,決定了線程池的行為和性能。主要參數如下:

  1. corePoolSize(核心線程數) 核心線程會一直保留(除非設置了 allowCoreThreadTimeOut),即使處于空閑狀態也不會被回收。線程數小于該值時,新任務會直接創建線程執行。

  2. maximumPoolSize(最大線程數) 線程池能容納的最大線程數。當任務數過多,阻塞隊列滿了且核心線程數已滿,才會嘗試創建非核心線程,直到達到最大線程數。

  3. keepAliveTime(空閑線程存活時間) 非核心線程在空閑狀態下,超過該時間會被回收。可以通過設置 allowCoreThreadTimeOut 為 true,讓核心線程也受到這個限制。

  4. unit(時間單位) 配合 keepAliveTime 使用,指定時間的單位,例如 TimeUnit.SECONDS。

  5. workQueue(任務隊列) 存放等待執行任務的隊列,有多種實現,如:

    • LinkedBlockingQueue(無界,常用于執行大量短期任務)

    • ArrayBlockingQueue(有界,常用于控制內存)

    • SynchronousQueue(不存儲任務,適用于任務很快就能執行)

  6. threadFactory(線程工廠) 用于創建新線程,通常用來自定義線程名字或設置為守護線程。

  7. handler(拒絕策略) 當線程池已滿且隊列也滿了,任務無法處理時的策略,常見有:

    • AbortPolicy(默認,拋出異常)

    • CallerRunsPolicy(由調用者線程執行)

    • DiscardPolicy(直接丟棄)

    • DiscardOldestPolicy(丟棄最舊的任務)

這些參數的組合決定了線程池的行為模型,合理配置可以提升并發性能,避免線程資源浪費或過載崩潰。

線程池的拒絕策略有哪些?

線程池的拒絕策略定義了在線程數達到 maximumPoolSize 且隊列已滿時,如何處理新提交的任務。JDK 提供了四種默認策略,都實現了 RejectedExecutionHandler 接口:

  1. AbortPolicy(默認) 直接拋出 RejectedExecutionException 異常,阻止系統繼續提交任務,讓調用者知道線程池已無法處理新任務。

  2. CallerRunsPolicy 由調用線程(提交任務的線程)來執行任務,不拋異常,避免任務丟失,但可能拖慢主線程速度。

  3. DiscardPolicy 直接丟棄任務,不拋異常,適合對部分任務容忍丟失的場景。

  4. DiscardOldestPolicy 丟棄隊列中最舊的任務,然后嘗試重新提交當前任務。如果任務提交速率過高,可能導致重要任務被踢出。

除了這四種,也可以自定義拒絕策略,實現 RejectedExecutionHandler 接口,根據業務需求編寫邏輯,比如記錄日志、發送告警等。

選擇哪種策略,需要根據業務容忍度、任務重要性和系統設計來權衡。

Executors 提供的幾種線程池?各有什么特點?

Executors 是 Java 提供的線程池工廠類,用于快速創建幾種常用線程池,雖然方便,但不推薦在生產中直接使用(理由稍后說)。它提供以下幾種線程池:

  1. newFixedThreadPool(int nThreads) 創建一個固定大小的線程池,核心線程數 = 最大線程數,使用 LinkedBlockingQueue(無界隊列)。 特點:線程數固定,適合負載穩定、線程數可控的場景。 缺點:任務過多可能導致內存撐爆(因為隊列是無界的)。

  2. newCachedThreadPool() 創建一個彈性線程池,線程數幾乎不受限,空閑線程60秒回收,使用 SynchronousQueue(不緩存任務)。 特點:適合執行大量短期異步任務。 缺點:高并發下容易創建過多線程,可能導致 OOM。

  3. newSingleThreadExecutor() 創建一個單線程線程池,只有一個核心線程,使用 LinkedBlockingQueue特點:所有任務串行執行,保證順序性,適合場景如日志寫入。 缺點:單線程一旦掛掉,所有任務都不能執行。

  4. newScheduledThreadPool(int corePoolSize) 創建一個支持定時和周期性任務的線程池,類似定時器功能,使用 DelayedWorkQueue特點:適用于定時任務、周期調度任務。 缺點:底層線程數不受限制,仍有內存風險。


為什么不推薦直接使用 Executors? 它們默認的隊列和線程數量配置不合理(如無界隊列、無限線程),在高并發下容易造成 OOM 或線程堆積。推薦自己使用 ThreadPoolExecutor 顯式設置參數,更安全、可控。阿里 Java 開發手冊也強烈建議這一點。

線程池的工作流程是怎樣的?

線程池的工作流程大致如下:

  1. 提交任務 線程池的工作從調用 execute()submit() 提交任務開始。任務被封裝成一個 RunnableCallable 對象,然后被提交到線程池。

  2. 任務排隊 任務提交后,首先進入線程池的任務隊列(如 BlockingQueue)。線程池會根據隊列的類型和大小決定任務是否能夠立即執行。常見的隊列類型有:

    • 無界隊列:任務一直可以加入隊列,不會拋出異常(例如 LinkedBlockingQueue)。

    • 有界隊列:當隊列滿時,任務會被拒絕(例如 ArrayBlockingQueue)。

    • 同步隊列:每個任務都會直接交給一個線程執行,不會被緩存(例如 SynchronousQueue)。

  3. 線程池工作線程執行任務 如果線程池中有空閑線程,它們會從隊列中取出任務并執行。如果沒有空閑線程:

    • 如果線程池中的線程數小于核心線程數(corePoolSize),線程池會創建一個新線程來執行任務。

    • 如果線程池中的線程數達到核心線程數,任務將被放入隊列,等待已有線程空閑。

    • 如果隊列已滿,且線程池中的線程數小于最大線程數(maximumPoolSize),線程池會創建新線程處理任務。

    • 如果線程池中的線程數已經達到最大線程數,且隊列也已滿,線程池會根據設定的拒絕策略(如 AbortPolicy, CallerRunsPolicy 等)處理任務。

  4. 任務完成和線程回收 當線程池中的線程執行完任務后,會進行線程回收:

    • 如果是非核心線程且空閑時間超過了 keepAliveTime,它們會被回收,線程池的線程數會減少。

    • 如果線程池的大小大于 corePoolSize,并且有空閑線程,這些線程會被銷毀,直到線程池的線程數等于核心線程數。

  5. 關閉線程池 當線程池的任務執行完畢或不再需要時,調用 shutdown()shutdownNow() 方法來關閉線程池。shutdown() 會等待任務執行完畢后再關閉,而 shutdownNow() 會嘗試停止正在執行的任務并返回未執行的任務。


總結: 線程池的工作流程是:

  • 提交任務 → 判斷線程池狀態(空閑線程/核心線程/最大線程數) → 執行任務或排隊等待 → 任務完成 → 線程回收。 線程池通過合理的資源管理和任務調度,優化了線程的創建、復用和銷毀,提升了系統的并發處理能力和性能。

并發工具類

CountDownLatch 和 CyclicBarrier 的區別?

CountDownLatchCyclicBarrier 都是 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 中的一個并發控制工具類,主要用于控制訪問某個資源的線程數量。它可以用來限制同時訪問某些資源的線程數,避免系統因為過多線程訪問共享資源而產生問題(如資源競爭、性能下降、線程安全問題等)。

主要作用:

  1. 限制資源訪問數量 Semaphore 通過維護一個計數器來控制同時訪問某個資源的線程數量。計數器的初始值代表可以同時訪問資源的最大線程數。如果有線程請求訪問資源,計數器的值減一;如果計數器值為零,新的線程就會被阻塞,直到有線程釋放資源(通過 release() 方法)。

  2. 實現并發控制 它用于實現類似“并發限制”的場景,如限制線程池的并發線程數、數據庫連接池、限制訪問某個共享資源的線程數量等。

  3. 避免線程過度競爭 在某些場景下,可以用 Semaphore 限制線程訪問某些資源,避免資源過度競爭導致的性能瓶頸或者線程崩潰。

核心方法:

  • acquire():獲取許可(即請求資源)。如果沒有足夠的許可,線程會被阻塞,直到有許可可用。

  • release():釋放許可,增加計數器的值,允許其他線程訪問資源。

  • availablePermits():返回當前可用的許可數量。

  • drainPermits():返回并清空當前所有可用的許可數量。

使用場景:

  1. 控制并發數:例如限制數據庫連接池中最大連接數、控制請求訪問頻率等。

  2. 并發限流:當資源訪問過于頻繁時,可以使用 Semaphore 來限制并發數,保護資源。

  3. 異步任務管理:可以通過 Semaphore 來限制同時執行的異步任務數量,防止系統過載。

總結:

Semaphore 主要用于控制并發訪問的數量,可以限制對共享資源的訪問。它通過獲取和釋放許可來實現線程間的同步與資源控制。

ThreadLocal 的原理和使用場景?

原理

ThreadLocal 是一個線程局部變量類,每個線程都會有自己的變量副本,線程之間不會共享這些副本。它通過每個線程內部維護一個 ThreadLocalMap 來實現這一點,ThreadLocalMapThreadLocal 作為鍵,線程對應的變量作為值存儲。當調用 get()set() 時,實際上操作的是當前線程的局部變量副本,而不是共享變量。

使用場景

  1. 避免共享數據的線程安全問題:當多個線程需要使用相同類型的數據時,使用 ThreadLocal 可以為每個線程提供獨立的副本,避免了線程間的共享問題和同步開銷。

  2. 性能優化:對于一些頻繁訪問的資源(如數據庫連接、日期格式化等),使用 ThreadLocal 可以避免多線程的同步沖突,提升性能。

  3. 線程獨立存儲數據:在 Web 開發中,可以用 ThreadLocal 存儲線程局部數據,比如每個請求的會話信息,避免不同請求間的數據共享。

簡而言之,ThreadLocal 用于確保每個線程都可以擁有自己獨立的存儲空間,適合處理線程獨立數據的場景。

Fork/Join 框架的作用?

Fork/Join 框架是 Java 7 引入的一個并行計算框架,旨在簡化并行任務的處理,特別是對于可以被分解成小任務并且最終結果可以合并的計算。其核心思想是分治法(Divide and Conquer),將大任務拆分為多個小任務,分別執行后再合并結果。

作用

  1. 并行化任務 Fork/Join 框架可以通過拆分大任務為多個小任務,并將小任務并行執行,從而有效地利用多核處理器,提高計算效率。

  2. 任務分解與合并 它適合用來處理那些能夠遞歸分解為小子問題的問題。每個子任務完成后,將其結果匯總或合并為最終結果。

  3. 任務調度優化 Fork/Join 框架通過使用工作竊取算法(Work Stealing)來優化任務調度。空閑的工作線程可以竊取其他線程未完成的任務,從而提高系統的吞吐量和資源利用率。

主要組件

  1. ForkJoinPool ForkJoinPoolFork/Join 框架的核心執行池,它繼承自 AbstractExecutorServiceForkJoinPool 具有工作竊取算法,可以有效管理多線程的任務執行。

  2. 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、版本號控制等synchronizedReentrantLock
適用場景并發訪問較少或沖突不頻繁的場景并發訪問較多,數據一致性要求高的場景

4. 總結

  • 悲觀鎖適用于數據沖突頻繁的場景,通過加鎖來防止數據沖突,但可能會帶來性能損失。

  • 樂觀鎖適用于數據沖突較少的場景,通過不加鎖的方式提高性能,但需要額外的沖突檢測機制(如CAS或版本控制)。

什么是死鎖?如何避免死鎖?

死鎖是什么?

死鎖(Deadlock)是指在多個線程之間發生一種特殊的情況,其中每個線程都在等待其他線程釋放它所需要的資源,但這些線程都無法繼續執行,導致程序進入一個永遠等待的狀態。簡而言之,死鎖發生時,程序中的所有線程都在相互等待資源,造成了無休止的等待,從而導致程序無法繼續執行。

死鎖的必要條件(四個條件)

死鎖通常是由以下四個條件共同作用導致的,它們被稱為死鎖的四個必要條件

  1. 互斥條件(Mutual Exclusion) 至少有一個資源處于“只允許一個線程訪問”的狀態,也就是說,某個資源只能被一個線程占用。

  2. 請求與保持條件(Hold and Wait) 一個線程持有某些資源,同時又請求其他資源,但是請求的資源被其他線程持有,造成線程阻塞,不能繼續執行。

  3. 不剝奪條件(No Preemption) 已分配給線程的資源在未使用完之前不能被強行剝奪,只能在線程完成任務后才釋放資源。

  4. 循環等待條件(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)中的一個抽象類,提供了一種基于隊列的同步器框架,用于構建自定義的同步工具(如 ReentrantLockCountDownLatchSemaphore 等)。它是實現各種同步機制的基礎框架,可以幫助開發者在并發編程中簡化鎖的實現。

AQS 的作用

AQS 提供了一個統一的抽象框架,用于實現同步器的基本操作,如請求鎖、釋放鎖、排隊等待等。它通過一個雙向隊列(FIFO 隊列)管理多個線程,確保線程按照請求的順序訪問共享資源。AQS 主要負責以下幾項工作:

  1. 隊列管理 AQS 使用一個隊列來管理線程的排隊。在同步操作過程中,如果某個線程無法立即獲取到鎖或資源,它會被放入隊列中,等待其他線程釋放資源后再進行獲取。AQS 管理這些線程的入隊、出隊、等待和喚醒操作。

  2. 線程的獲取與釋放 AQS 提供了獲取和釋放同步資源的基本操作,如 acquire()release()。通過繼承 AQS 并實現其中的抽象方法,開發者可以根據自己的需求定制不同的同步器。

  3. 共享和獨占模式 AQS 支持兩種不同的同步模式:獨占模式和共享模式。

    • 獨占模式:某個線程獲取資源后,其他線程無法獲取該資源,直到持有資源的線程釋放。

    • 共享模式:多個線程可以同時獲取資源,直到資源達到上限才會阻塞等待。

  4. 線程阻塞與喚醒 AQS 管理線程的阻塞與喚醒機制。如果當前線程無法獲取到資源,它會被加入隊列并進入阻塞狀態。其他線程釋放資源后,會喚醒隊列中的線程,使其能夠繼續執行。

  5. 底層支持自定義同步器 通過繼承 AQS 并實現其方法,開發者可以輕松實現自定義的同步器。比如 ReentrantLockCountDownLatchSemaphoreReadWriteLock 等都可以通過 AQS 來實現。

AQS 的核心方法

  • acquire(int arg): 請求獲取資源,并嘗試根據給定的參數(如嘗試次數、超時等)獲得同步資源。通常用于實現鎖的獲取邏輯。

  • release(int arg): 釋放資源,表示當前線程完成任務后釋放鎖或者同步資源。通常用于實現鎖的釋放邏輯。

  • tryAcquire(int arg): 嘗試獲取資源,通常會被自定義同步器重寫,以決定是否能夠立即獲取鎖。

  • tryRelease(int arg): 嘗試釋放資源,通常會被自定義同步器重寫,執行一些資源釋放后的后處理操作。

  • acquireShared(int arg): 共享模式下請求資源。通常用于如信號量等共享資源的獲取。

  • releaseShared(int arg): 共享模式下釋放資源,通常用于信號量等資源的釋放。

AQS 的工作原理

  1. 隊列和線程阻塞 當一個線程請求獲取資源時,如果該資源當前不可用,線程將被加入到 AQS 的等待隊列中。線程進入等待狀態,直到有線程釋放資源,并喚醒它。

  2. 資源的競爭和獲取 資源的獲取通常由 tryAcquiretryAcquireShared 方法實現。如果這些方法成功獲取了資源,線程就可以開始執行。如果失敗,則進入隊列,等待被喚醒。

  3. 資源的釋放 線程完成任務后,通過 releasereleaseShared 方法釋放資源。此時,AQS 會嘗試喚醒隊列中的其他線程,讓它們有機會獲取資源。

  4. 隊列的管理 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 的鎖優化是動態的,并且會根據線程競爭的情況做出相應的調整。

什么是自旋鎖?

自旋鎖是一種同步機制,它通過不斷循環檢查某個條件(例如鎖的狀態)來獲取鎖,而不是讓線程進入阻塞狀態。線程在獲取鎖時,如果發現鎖已被其他線程占用,它不會立即放棄,而是會持續檢查鎖是否釋放,直到獲得鎖為止。

自旋鎖的特點:

  1. 無阻塞:自旋鎖不會讓線程進入阻塞狀態,它讓線程持續檢查鎖的狀態。這樣避免了線程阻塞和喚醒的開銷,減少了上下文切換。

  2. 適用于鎖持有時間短的場景:當鎖的持有時間很短時,自旋鎖比阻塞式鎖(如 synchronized)更高效,因為它避免了線程的上下文切換。

  3. CPU消耗:在高并發情況下,線程會一直自旋等待鎖,導致占用大量 CPU 資源。如果鎖的持有時間過長,CPU 會被浪費掉。

  4. 沒有公平性保證:自旋鎖不能保證先到的線程優先獲取鎖,可能會導致某些線程一直無法獲取到鎖。

自旋鎖的應用場景:

  1. 鎖持有時間短的場景:自旋鎖適合用于鎖持有時間非常短的場景,例如一個線程執行的操作只需要幾次 CPU 時鐘周期就能完成。

  2. 高并發的場景:在某些高并發場景下,如果大部分時間只有一個線程能成功獲取鎖,其他線程可以通過自旋快速等待,避免了不必要的上下文切換。

  3. 避免線程阻塞:自旋鎖能避免線程被掛起,特別適用于鎖競爭較小的場景。

自旋鎖的缺點:

  1. CPU占用高:如果鎖的持有時間較長,線程將會長時間自旋,占用大量 CPU 資源,降低系統的性能。

  2. 鎖競爭激烈時不適用:當多個線程頻繁競爭鎖時,自旋鎖可能會導致嚴重的性能問題,因為線程會一直消耗 CPU 資源。

  3. 沒有公平性:自旋鎖不保證鎖的獲取順序,因此可能會出現某些線程長時間無法獲得鎖的情況。

總的來說,自旋鎖在某些場景下能提高性能,但在鎖競爭激烈或鎖持有時間長的情況下,它的缺點會非常明顯,因此使用時需要謹慎。

JMM(Java 內存模型)

什么是 JMM?

JMM(Java Memory Model,Java內存模型)是 Java 程序中線程間通信和共享數據的規范,它定義了 Java 程序中不同線程如何訪問共享變量、如何同步變量的值,以及如何確保多線程程序的正確執行。JMM 主要目的是確保 Java 程序的可見性、原子性和有序性,以便在多線程環境下避免出現不可預料的行為。

JMM的核心概念

  1. 內存共享: 在 Java 中,所有線程共享一塊內存區域。每個線程有自己的工作內存,線程的工作內存存儲了它所使用的變量副本,而共享變量存儲在主內存中。線程對共享變量的讀寫操作必須通過主內存來進行。

  2. 主內存和工作內存

    • 主內存:存儲所有線程共享的變量,線程通過主內存進行讀寫操作。

    • 工作內存:每個線程有自己的工作內存,工作內存是線程對變量的本地副本,線程在工作內存中操作共享變量。

  3. JMM的目標: JMM 的設計目標是確保多線程編程中共享變量的可見性、原子性有序性

    • 可見性:當一個線程修改了共享變量的值,其他線程能及時看到這個修改。

    • 原子性:一個操作要么全部執行成功,要么完全不執行,不會受到其他線程的干擾。

    • 有序性:程序中代碼的執行順序必須符合語義,避免由于指令重排序導致的異常行為。

JMM的關鍵規則

  1. 主內存與工作內存的交互規則

    • 讀取共享變量:當線程要讀取共享變量時,必須通過工作內存讀取主內存中的變量。

    • 寫入共享變量:當線程要修改共享變量時,必須將修改的值寫入主內存。

  2. volatile變量

    • 使用 volatile 關鍵字修飾的變量,能夠保證線程對該變量的修改對其他線程是可見的。即當一個線程修改了 volatile 變量的值,其他線程立即能夠看到該變化。

    • 但是,volatile 并不能保證復合操作的原子性,像 ++ 這種操作仍然會出現線程安全問題。

  3. Happens-Before原則: JMM定義了happens-before原則,用于確定線程間的操作順序。它描述了不同線程之間的操作是否能夠保證順序執行。

    • 程序順序規則:一個線程內的操作按照程序的順序執行。

    • 鎖規則:在進入某個鎖(如 synchronized)之前的操作,happens-before 鎖釋放之后的操作。

    • volatile規則:對 volatile 變量的寫操作 happens-before 任何后續對該變量的讀操作。

  4. 重排序和指令重排: JMM允許一定程度的指令重排序,以提高性能。但這可能會導致程序執行結果與預期不一致。為了避免不必要的重排序,JMM通過同步機制(如 synchronizedvolatile)來保證線程之間的正確執行順序。

JMM的內存可見性問題

在多線程環境中,由于每個線程有自己的工作內存,線程對共享變量的修改在某些情況下可能不會及時傳遞到其他線程。例如:

  • 線程1修改共享變量A的值,但線程2未能及時讀取到線程1修改后的最新值。這種情況稱為內存可見性問題。

解決這個問題的一些方法包括:

  • 使用 volatile 關鍵字,確保修改立即對其他線程可見。

  • 使用 synchronized 塊,確保對共享變量的訪問是同步的,避免內存可見性問題。

JMM的原子性和有序性

  1. 原子性:JMM確保一些基本操作(如讀取、寫入)是原子的,但像 ++ 這樣的復合操作不是原子的,需要通過同步機制(例如 synchronizedAtomic 類)來保證原子性。

  2. 有序性:JMM通過允許一定程度的指令重排來提高性能。但為了避免重排序導致的錯誤,需要使用同步機制來確保正確的執行順序。比如 synchronizedvolatile 可以確保有序性。

總結

JMM定義了 Java 程序中多線程如何正確地共享數據,它通過規定內存模型的規則,保證了線程間的可見性、原子性和有序性。理解 JMM 可以幫助我們更好地設計多線程程序,避免常見的并發問題。

happens-before 原則是什么?

Happens-Before原則是Java內存模型(JMM)中定義的線程操作順序的規則,它保證了多線程環境下線程之間的操作順序及可見性。它的基本意思是:一個操作必須發生在另一個操作之前,從而確保前一個操作的結果能被后續操作看到。

Happens-Before的核心規則

  1. 程序順序規則:一個線程內的操作總是按照程序順序執行的,也就是說,前面的操作 happens-before 后面的操作。

  2. 鎖規則:在多個線程之間,如果一個線程釋放了鎖,那么另一個線程在獲取該鎖時,釋放鎖的操作 happens-before 獲取鎖的操作。這樣,第二個線程能夠看到第一個線程對共享變量的修改。

  3. volatile規則:對 volatile 變量的寫操作 happens-before 任何后續對該變量的讀操作。即當一個線程修改了 volatile 變量的值,其他線程立刻可以看到修改后的值。

  4. 線程啟動規則:當一個線程調用另一個線程的 start() 方法時,start() 操作 happens-before 被啟動線程的任何其他操作,確保啟動線程的狀態是可見的。

  5. 線程結束規則:當一個線程調用 join() 方法等待另一個線程結束時,join() 操作 happens-before 被調用線程的結束操作。確保前一個線程的執行完成,后續的線程才能繼續。

  6. 中斷規則:線程的中斷操作 happens-before isInterrupted() 檢查。

為什么Happens-Before很重要?

Happens-Before原則定義了線程之間的操作順序,確保了在多線程程序中,線程間的共享數據的一致性和可見性。它幫助開發者理解在不同線程之間如何正確地同步數據,從而避免線程安全問題。

什么是內存可見性問題?如何解決?

內存可見性問題是指在多線程環境下,一個線程對共享變量的修改,其他線程可能無法立即看到該修改的情況。這種問題會導致線程間的同步失效,造成程序的行為不可預測。

內存可見性問題的原因:

  1. 線程本地緩存:現代處理器為提高性能,會對線程的工作內存進行優化,每個線程都有自己的一塊工作內存,線程對共享變量的修改可能只會影響到自己本地的緩存,而不會立刻同步到主內存中,導致其他線程無法看到這個修改。

  2. CPU重排序:為了提高執行效率,CPU可能會對指令進行重排序,這可能會改變程序中操作的執行順序,導致一個線程對共享變量的修改在另一個線程訪問之前不可見。

  3. 不適當的同步:如果多個線程對共享數據進行操作時沒有適當的同步機制,也會導致線程間的共享數據不一致,無法保證修改的可見性。

如何解決內存可見性問題?

  1. 使用 volatile 關鍵字volatile 關鍵字保證了對該變量的修改對所有線程是可見的。即每次讀取 volatile 變量時,都會直接從主內存中讀取最新的值,而不是從線程的工作內存中讀取。這就確保了多個線程間對該變量的修改能夠及時被其他線程看到。

  2. 使用 synchronized 關鍵字synchronized 關鍵字通過加鎖來確保共享數據的可見性和原子性。每個線程在進入一個同步塊時,必須首先獲取鎖,并在執行完同步塊后釋放鎖。這樣,線程之間的共享變量修改會通過鎖的機制同步到主內存,從而保證數據的可見性。

  3. 使用 java.util.concurrent 包中的工具類

    • Atomic:例如 AtomicIntegerAtomicBoolean 等類提供了原子性操作,保證線程安全,并且它們的操作通常是直接通過硬件提供的原子操作實現的。

    • CountDownLatchCyclicBarrier 等同步工具:這些工具類能夠幫助線程之間協調執行順序,從而保證共享變量在不同線程之間的可見性。

  4. 使用 final 關鍵字: 對于一些不可變的對象,使用 final 關鍵字可以保證線程安全。當一個對象被標記為 final 時,JVM 會保證該對象的初始化操作是線程安全的,也就是說,final 字段一旦賦值,其他線程就能夠立即看到該字段的值。

  5. 使用內存屏障(Memory Barrier): 內存屏障是硬件或操作系統提供的底層機制,用來防止指令重排序,確保操作的順序性。雖然這種機制通常由 JVM 和底層硬件處理,但我們可以通過使用高層次的同步機制(如 volatilesynchronized)來間接控制內存屏障的使用。

總結

內存可見性問題會導致多線程程序出現不可預知的錯誤。為了解決這個問題,我們可以通過使用 volatilesynchronized 等同步機制,確保線程間對共享變量的修改能夠及時且正確地傳播到其他線程,從而保證程序的一致性和可靠性。

final 關鍵字在多線程中的作用?

抱歉,下面我不會再提供代碼,只會解釋概念。

在多線程中,final 關鍵字的作用主要體現在以下幾個方面:

1. 保證對象的安全發布

在多線程環境下,當多個線程共享同一個對象時,確保對象在構造完成后能夠正確地被其他線程看到是至關重要的。final 關鍵字能夠保證一個對象在構造完成后,其值對其他線程是可見的。這是因為 final 修飾的字段保證了在對象構造過程中,不會出現字段的重排序或值的延遲寫入,使得構造完成后的對象狀態對其他線程是一致的。

2. 創建不可變對象

final 可以用于字段,確保字段在對象構造后不可修改。通過 final,可以創建不可變對象(Immutable Object)。不可變對象的狀態在構造完成后不能被改變,因此在多線程環境下,不需要擔心多個線程同時修改對象的狀態,會造成數據不一致的問題。這種特性使得不可變對象天然具有線程安全性。

3. 防止重排序

final 關鍵字還與JVM的內存模型緊密相關。它能確保變量在構造時的初始化順序,防止指令重排序(即,JVM或CPU為了優化性能而調整指令執行順序的行為)。這樣,可以保證在構造過程中,final 變量的值在對象構造完成之前不會發生變化,確保其他線程能夠正確地看到該變量的值。

總結

final 關鍵字在多線程中的作用主要是:

  • 保證對象構造完成后,其字段在所有線程中都是可見的;

  • 使得對象不可修改,從而避免多個線程修改對象狀態造成的競爭條件;

  • 防止指令重排序,確保變量初始化順序,避免可見性問題。

這些特點使得 final 成為實現線程安全和正確發布共享變量的重要工具。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/78045.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/78045.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/78045.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Qt/C++】QPrinter關于QInternal::Printer的解析

1. 問題分析 QInternal::Printer在Qt框架中并不是一個直接暴露給用戶的API。相反,它是一個枚舉值,用于標識QPaintDevice的類型。在Qt中,QPaintDevice是一個抽象類,用于任何可以進行繪制的設備,如窗口、圖像、打印機等…

uniapp返回上一頁接口數據更新了,頁面未更新

注意:不是組件套組件可以不使用setTimeout延時 返回上一頁一般會走onshow,但是接口更新了頁面未更新 onShow(() > {// 切換城市后重新調用數據if (areaId.value) {const timer setTimeout(async () > {timer && clearTimeout(timer);…

MCU開發學習記錄11 - ADC學習與實踐(HAL庫) - 單通道ADC采集、多通道ADC采集、定時器觸發連續ADC采集 - STM32CubeMX

名詞解釋: ADC: Analog-to-Digital SAR:Successive Approximation Register 本文將介紹ADC的概念、相關函數以及STM32CubeMX生成ADC的配置函數。針對于ADC實踐:單通道采集芯片內部溫度傳感器(ADC1_ch16)&a…

68元撬動未來:明遠智睿2351開發板重塑嵌入式開發生態

在嵌入式開發領域,價格與性能的矛盾始終存在:高端開發板功能強大但成本高昂,低價產品則往往受限于性能與擴展性。明遠智睿2351開發板以68元(含稅)的定價打破這一僵局,通過四核1.4G處理器、全功能Linux系統與…

關于ubuntu密碼正確但是無法登錄的情況

參考這個文章: https://blog.csdn.net/cuichongxin/article/details/117462494 檢查一下是不是用戶被lock了 輸入passwd -s username 如果用戶是L狀態,那么就是lock了。 使用 passwd -u username 解鎖 關于 .bashrc 不生效 有幾點: ~/.…

LeetCode-47. 全排列 II

1、題目描述: 給定一個可包含重復數字的序列 nums ,按任意順序 返回所有不重復的全排列。 示例 1: 輸入:nums [1,1,2] 輸出: [[1,1,2],[1,2,1],[2,1,1]]示例 2: 輸入:nums [1,2,3] 輸出&am…

Python 設計模式:訪問者模式

1. 什么是訪問者模式? 訪問者模式是一種行為設計模式,它允許你在不改變對象結構的前提下,定義新的操作。通過將操作封裝在訪問者對象中,訪問者模式使得你可以在不修改元素類的情況下,向元素類添加新的功能。 訪問者模…

基于stm32的智能門鎖系統

標題:基于stm32的智能門鎖系統 內容:1.摘要 摘要:隨著科技的飛速發展,人們對家居安全的要求日益提高,智能門鎖系統應運而生。本研究的目的是設計并實現一個基于STM32的智能門鎖系統。采用STM32微控制器作為核心控制單元,結合指紋…

GitHub 常見高頻問題與解決方案(實用手冊)

目錄 1.Push 提示權限錯誤(Permission denied) 2.push 報錯:rejected non-fast-forward 3.忘記添加 .gitignore,上傳了無關文件 4. 撤銷最近一次 commit 5.clone 太慢或失敗 6.如何切換/創建分支 7.如何合并分支 8.如何刪除遠程分支 9.如何 Fork + PR(Pull Reque…

【MySQL數據庫入門到精通-04 DML操作】

一、DML DML英文全稱是Data Manipulation Language(數據操作語言),用來對數據庫中表的數據記錄進行增、刪、改操作。 二、添加數據 1.給指定字段添加數據 代碼如下(示例): insert into 表名 (字段1,字…

2022 年 9 月青少年軟編等考 C 語言六級真題解析

目錄 T1. 棧的基本操作T2. stack or queue思路分析T3. 合影效果T4. 發型糟糕的一天思路分析T1. 棧的基本操作 題目鏈接:SOJ D1188 此題為 2022 年 6 月三級第二題僅有棧操作的版本,見 2022 年 6 月青少年軟編等考 C 語言三級真題解析中的 T2。 T2. stack or queue 題目鏈…

美創市場競爭力突出!《2025中國數據安全市場研究報告》發布

數據要素時代,數據已成國家戰略性資源,數據安全關乎國家安全!數說安全發布的《2025中國數據安全市場研究報告》(以下簡稱《報告》)顯示,2024年數據安全市場逆勢增長,市場規模首次突破百億。《報…

VUE Element-ui Message 消息提示組件自定義封裝

為了讓message 信息提示的更加方便快捷,減少不同地方的調用,避免代碼的重復,特意再官方message 組件的基礎上二次封裝,使代碼更加的優雅和高效。 實現效果: 代碼組件: 封裝成 message.js 文件,…

高防IP能抵御哪些類型的網絡攻擊?

高防IP(High Defense IP)是一種專門針對網絡攻擊設計的防護服務,主要通過流量清洗、協議分析、行為檢測等技術抵御多種網絡攻擊。以下是其能防御的主要攻擊類型及原理: ??一、常見防御的攻擊類型?? ??DDoS攻擊(分…

小紅書文字配圖平替工具

小紅書的文字配圖只有手機版有,想找一個電腦版的,查了一下。以下是幾款類似小紅書風格的花字、藝術字生成工具,適合制作吸睛的社交媒體配圖,分為 手機APP 和 在線工具 兩類,供你選擇: 一、手機APP推薦 醒圖…

【浙江大學DeepSeek公開課】走向數字社會:從DeepSeek到群體智慧

從DeepSeek到群體智慧 一、人工智能發展脈絡二、DeepSeek大模型的意義與特點三、人工智能促進社會數字化轉型四、群體智慧與數字社會 一、人工智能發展脈絡 圖靈與圖靈機:1937年,圖靈發表論文《On computable numbers, with an application to the Ents…

解讀大型語言模型:從Transformer架構到模型量化技術

一、生成式人工智能概述 生成式人工智能(Generative Artificial Intelligence)是一種先進的技術,能夠生成多種類型的內容,包括文本、圖像、音頻以及合成數據等。其用戶界面的便捷性極大地推動了其廣泛應用,用戶僅需在…

JSON實現動態按鈕管理的Python應用

在開發桌面應用程序時,動態生成用戶界面元素并根據配置文件靈活管理是一項常見需求。本文將介紹如何使用Python的wxPython庫結合JSON配置文件,開發一個支持動態按鈕創建、文件執行和配置管理的桌面應用程序。該應用允許用戶通過設置界面配置按鈕名稱和關…

序章:寫在前面

目錄 為什么要學習 Python?那么,Python 到底是什么呢?Python 的用戶多嗎?Python 的語法究竟是怎樣的?C 語言JavaPython Python 好學嗎? 為什么要學習 Python? 這個問題或許會讓不少人感到不解。…

onlyoffice歷史版本功能實現,版本恢復功能,編輯器功能實現 springboot+vue2

文章目錄 oonlyoffice歷史版本功能實現 (編輯器功能實現)springbootvue2前提 需要注意把這個 (改成自己服務器的ip或者域名) 改成 自己服務器的域名或者地址1. onloyoffice 服務器部署 搜索其他文章2. 前段代碼 vue 22.1 需要注意把這個 (改成自己服務器…