Java并發核心基礎解析

目錄

一、背景

二、Java線程模型

三、Synchronized實現原理

3.1 鎖的使用

3.2 解釋執行

3.3 JIT執行

3.4 鎖的狀態

3.5?monitorenter

3.5.1 偏向鎖

3.5.2 輕量級鎖

3.5.3 重量級鎖

3.6?monitorexit

3.6.1 偏向鎖

3.6.2 輕量級鎖

3.6.3 重量級

四、可見性的真相

4.1 緩存一致性協議

4.2 根因分析

五、CAS實現原理

5.1 CAS介紹

5.2 CAS特性

5.3 CAS實現原理

六、Volatile

6.1 字節碼

6.2 屏障類型

6.3?volatile修飾的寫操作

6.4?volatile修飾的讀操作

6.5 如何保證可見性

七、Reentrantlock實現原理

7.1 AQS

7.2 lock

7.3 unlock

7.4 CLH隊列

7.5 與?synchronized?關鍵字的對比

7.5 可中斷鎖和超時鎖

7.6 Condition

八、線程阻塞或喚醒時操作系統的動作

九、原子性、有序性、可見性的保證


一、背景

講解一下在java中涉及到并發的相關基礎知識,深入理解 synchronized, volatile, CAS, ReentrantLock 與內存可見性、原子性、有序性

二、Java線程模型

現代JVM(如HotSpot)默認采用此模型:每個Java線程直接綁定一個操作系統線程(內核線程)

  • 優點

    • 利用多核CPU并行執行。

    • 線程阻塞(如I/O)不影響其他線程。

  • 缺點

    • 線程創建/銷毀開銷大(需OS介入)。

    • 線程數量受限于OS(默認Linux約數千個)。

三、Synchronized實現原理

3.1 鎖的使用

Java 使用synchronized關鍵字加鎖,

Object lock = new Object();
synchronized (lock) {// 代碼塊
}

加上鎖的代碼會被編譯成如下的字節碼:

0: new           #2      // 創建 Object 對象
3: dup
4: invokespecial #1      // 調用 Object 構造方法
7: astore_1             // 存儲到局部變量 lock8: aload_1              // 加載 lock 到操作數棧
9: dup
10: astore_2            // 存儲鎖對象副本
11: monitorenter        // 嘗試獲取鎖
12: aload_2
13: monitorexit         // 正常釋放鎖
14: goto 20
17: aload_2
18: monitorexit         // 異常時釋放鎖 (確保鎖被釋放)
19: athrow
20: return

我們后面主要研究下?monitorenter? 和??monitorexit 的底層原理,了解這兩個字節碼,我們首先要了解,它們是如何被java執行的

3.2 解釋執行

“解釋執行”的核心含義:

  1. 執行單元是單條字節碼指令:?解釋器(包括模板解釋器)的基本工作單元是一條字節碼指令。它逐條讀取、解釋(分派)并執行字節碼指令。

  2. 沒有預先的“完整編譯”:?在執行一個 Java 方法之前,解釋器不會把這個方法包含的所有字節碼指令作為一個整體編譯成一段完整的、連續的、優化過的本地機器碼。它只為每指令準備了模板。

  3. 邊“解釋”(分派)邊執行:?執行過程始終伴隨著一個分派循環

    • 取指:?從當前方法的字節碼流中讀取下一條指令的操作碼。

    • 分派:?根據這個操作碼,查找或計算對應的機器碼模板的入口地址。

    • 跳轉執行:?跳轉到該模板地址,執行對應的機器碼片段。

    • 循環:?執行完這條指令的模板后,必須回到分派循環的開頭,重復上述步驟處理下一條指令。

  4. 執行上下文依賴分派循環:?每條指令的機器碼模板執行完畢后,控制權必須交還給解釋器的分派循環。這個循環負責維護執行狀態(如程序計數器 PC、棧指針等),并決定下一條要執行的指令是什么。模板本身通常不包含跳轉到下一條指令的邏輯(除了像?goto?這種控制流指令,它們會直接修改 PC)。

3.3 JIT執行

  • JIT 編譯:?當 HotSpot 發現某個方法是“熱點”時,它的 JIT 編譯器(如 C1, C2)會介入。

    • 執行單元:?它將整個方法(或一個熱點循環)作為一個單元。

    • 過程:?讀取該方法的所有相關字節碼,進行復雜的靜態分析優化(如寄存器分配、死代碼消除、循環展開、方法內聯、逃逸分析等),最終生成一段完整的、連續的、高度優化的本地機器碼

    • 執行:?當再次調用這個方法時,JVM?直接跳轉到這段編譯好的機器碼的起始地址。這段機器碼自己負責執行整個方法的邏輯,包括控制流(跳轉、循環、調用)。它完全繞過了解釋器的分派循環。執行過程中不再有“取指-分派”的開銷,并且代碼是優化過的。

  • 關鍵區別:?JIT 編譯后執行的是一個完整優化后的代碼塊,而模板解釋器執行的是一系列獨立的、通過分派循環粘合起來的機器碼片段

通過上面的解釋,我們大概能理解,java執行時是邊解釋字節碼邊執行的,而不是直接翻譯成機器碼文件。

java會把?monitorenter? 和??monitorexit解釋成相關的機器碼,JVM執行時會跳到monitorenter 的機器碼的位置,進行執行

例如對于monitorenter? :

  • 當解釋器執行到?monitorenter?字節碼時:

    • 分派循環跳轉到?monitorenter?的機器碼模板入口。

    • CPU 執行模板中的指令:準備好參數(對象引用),然后執行?call?指令。

    • CPU 跳轉到?InterpreterRuntime::monitorenter?函數的機器碼(這個函數本身是 C++ 寫的,在 JVM 啟動時已經被編譯成了機器碼)。

    • InterpreterRuntime::monitorenter?的機器碼執行復雜的鎖邏輯(可能調用更底層的?ObjectMonitor::enter?等)。

    • InterpreterRuntime::monitorenter?函數執行完畢,通過?ret?指令返回到調用它的地方——也就是?monitorenter?模板中?call?指令的下一條指令。

    • monitorenter?模板中剩余的指令(如果有)執行。

    • 控制權返回到解釋器的分派循環,準備執行下一條字節碼。

下面來看下monitorenter和monitorexit的具體動作

3.4 鎖的狀態

📐 對象頭組成及大小(以主流64位系統為例)

??組成部分??

??大小(字節)??

??說明??

??是否可變??

??Mark Word??

8

存儲對象哈希碼、鎖狀態、GC年齡等信息

? 固定大小

??Klass Pointer??

4(或8)

指向類元數據的指針(默認指針壓縮)

? 可配置

??數組長度(可選)??

4

僅數組對象存在(存儲數組長度)

? 固定大小

在不同鎖狀態下,保存內容如下所示

3.5?monitorenter

java8的鎖粒度有 偏向鎖、輕量級鎖、重量級鎖

3.5.1 偏向鎖

  1. 檢查對象頭中的?Mark Word

    • 若可偏向(偏向模式位?1,鎖標志位?01)且?線程ID指向當前線程:直接進入同步塊(無CAS)。

    • 若可偏向但?線程ID不指向當前線程:觸發偏向鎖撤銷(需全局安全點),升級為輕量級鎖。

    • 若未偏向:通過?CAS?將 Mark Word 的線程ID設置為當前線程。

撤銷偏向鎖過程:

  1. 觸發條件
    當線程A嘗試獲取偏向線程B的鎖時(對象頭Mark Word中的線程ID ≠ 當前線程ID),觸發撤銷。

  2. 暫停所有Java線程(STW)

    • JVM 觸發?全局安全點(Safepoint),暫停所有Java線程(包括持有偏向鎖的線程B和競爭線程A)。

    • 關鍵原因:對象頭的Mark Word和持有鎖線程B的棧幀狀態需被原子修改,避免并發沖突。

  3. 撤銷操作(由JVM在安全點執行)

    • 步驟1:檢查持有偏向鎖的線程B的狀態:

      • 若線程B?已退出同步塊(無活躍鎖):

        • 直接重置對象頭為?無鎖狀態(鎖標志位?01,偏向模式?0)。

      • 若線程B?仍處于同步塊中(活躍鎖):

        • 將鎖升級為?輕量級鎖

          1. 在線程B的棧幀中生成鎖記錄(Lock Record),拷貝原Mark Word。

          2. 用CAS將對象頭的Mark Word替換為指向該鎖記錄的指針(鎖標志位?00)。

        • 若線程B已銷毀:強制釋放偏向鎖。

  4. 恢復線程并升級競爭

    • 安全點結束后,所有線程恢復執行。

    • 競爭線程A重新嘗試獲取鎖:

      • 此時對象頭已是輕量級鎖狀態(標志位?00),線程A通過?CAS自旋?競爭輕量級鎖。

3.5.2 輕量級鎖

  1. 在棧幀中創建?鎖記錄(Lock Record),拷貝對象頭的 Mark Word(稱為?Displaced Mark Word)。

  2. 通過?CAS?將對象頭的 Mark Word 替換為指向鎖記錄的指針(鎖標志位?00)。

    • 成功:獲得鎖。

    • 失敗(其他線程已占用):自旋重試;若自旋失敗,升級為重量級鎖。

cas操作的值如下:

  • 目標內存地址:對象頭的 Mark Word

  • 預期值(Expected Value):對象原始的 Mark Word(從棧幀的鎖記錄中獲取),對象原始的 Mark Word一般是偏向鎖或者無鎖狀態

  • 新值(New Value):指向當前線程棧幀中鎖記錄的指針 + 輕量級鎖標志位?00

3.5.3 重量級鎖

  • 觸發條件
    當線程嘗試獲取輕量級鎖時,如果 CAS 失敗(表示鎖已被占用),線程會進入自旋重試狀態。

    • 若自旋超過閾值仍失敗(默認約 10-50 次,JVM 自適應調整)

    • 或自旋期間有第三個線程加入競爭

參考圖:

  • 升級動作
    JVM 調用?inflateLock()?方法,創建?ObjectMonitor?對象(重量級鎖結構),修改對象頭標志位為?10

Monitor 對象對象結構:

  1. 檢查對象關聯的?Monitor 對象(位于對象頭指向的?ObjectMonitor)。

  2. 調用?ObjectMonitor::enter()

    • 若 Owner 為?null:通過 CAS 設置 Owner 為當前線程。

    • 若 Owner 是當前線程:重入計數?_recursions++

    • 否則:線程進入?阻塞隊列(cxq/EntryList,等待喚醒。

3.6?monitorexit

3.6.1 偏向鎖

  1. 不修改對象頭(保留偏向狀態)。

  2. 僅檢查線程ID是否匹配(防止錯誤解鎖)。

3.6.2 輕量級鎖

通過?CAS?將?Displaced Mark Word?寫回對象頭。

  • 成功:鎖釋放。

  • 失敗:表明已升級為重量級鎖,需走重量級鎖釋放流程

3.6.3 重量級

  1. 調用?ObjectMonitor::exit()

    • 重入計數?_recursions--

    • 若?_recursions == 0:清空 Owner,喚醒阻塞隊列中的線程。

重量級鎖變化舉例:

四、可見性的真相

4.1 緩存一致性協議

多線程并發修改變量時會有可見性問題

雖然我們有緩存一致性協議,具體如下圖,但它只能保證最終一致性,而不能保證中間過程的一致性:

4.2 根因分析

已知:

當CPU修改共享變量時:

  1. 使其他CPU的緩存行失效(I狀態)

  2. 修改自己的緩存(M狀態)

  3. 最終寫回主內存

分析:

原因1:寫緩沖(Store Buffer)

現代CPU使用寫緩沖優化性能:

plaintext

CPU-A 操作流程:
1. b的新值存入寫緩沖  ← 立即繼續執行后續指令
2. 異步將寫緩沖刷新到緩存(此時才觸發MESI)

在步驟1→2的間隙中:

  • CPU-C讀取b時,由于失效請求未到達,可能仍讀取自己的舊緩存

  • 即使CPU-A認為"修改已完成",實際修改仍在緩沖中未提交

原因2:失效隊列(Invalidation Queue)

plaintext

CPU-C 操作流程:
1. 收到b的失效請求 → 存入失效隊列
2. 繼續使用本地緩存(直到處理失效隊列) ← 關鍵延遲點!
3. 后續讀取才從主內存重新加載

在步驟1→3期間,CPU-C仍可能使用已失效的緩存值。

原因3:指令重排:

// 共享變量
int a = 0;
boolean flag = false; // 注意:非volatile!// 線程1(核心1執行)
a = 42;       // 語句1
flag = true;  // 語句2// 線程2(核心2執行)
while(!flag); // 語句3
System.out.println(a); // 可能輸出0! 

具體如何解決可見性問題,我們后面分析

五、CAS實現原理

5.1 CAS介紹

CAS(Compare-And-Swap)?是一種基于硬件的原子操作,用于實現無鎖(lock-free)并發編程。它是 Java 并發包(java.util.concurrent)中原子類(如?AtomicIntegerAtomicReference?等)的核心實現機制。

CAS 操作包含三個參數:

  1. 內存地址?V(需要更新的變量)

  2. 期望值?A(變量當前應具有的值)

  3. 新值?B(需要設置的新值)

操作邏輯

java

if (V == A) {V = B; // 更新值return true; // 成功
} else {return false; // 失敗
}

整個過程由硬件(CPU 指令)保證原子性,不會被線程調度打斷。

5.2 CAS特性

  1. 原子性(Atomicity)

    • 操作不可分割,要么完全執行成功,要么完全不執行。

    • 由底層 CPU 指令(如 x86 的?CMPXCHG)直接支持,無需鎖。

  2. 無鎖(Lock-Free)

    • 線程通過循環重試(自旋)更新數據,避免阻塞。

    • 示例代碼:

      java

      public final int incrementAndGet(AtomicInteger atomicInt) {int prev, next;do {prev = atomicInt.get(); // 當前值next = prev + 1;        // 新值} while (!atomicInt.compareAndSet(prev, next)); // CAS 失敗則重試return next;
      }
  3. 可見性(Visibility)

    • CAS 操作隱含?volatile?語義,確保修改對其他線程立即可見。

  4. 避免死鎖

    • 無鎖機制天然規避了死鎖風險。

5.3 CAS實現原理

底層 CPU 硬件指令直接保證的

  1. x86/x86-64 架構

    • 指令:LOCK CMPXCHG

      • CMPXCHG(Compare and Exchange)是基礎指令

      • LOCK?前綴(匯編指令前綴)強制獨占內存訪問,確保原子性

      • 工作流程:

        assembly

        ; 偽匯編代碼
        LOCK CMPXCHG [memory], reg   ; [memory]為內存地址,reg為新值
        ; 比較 EAX(隱含寄存器)與 [memory] 的值
        ; 相等 → 將 reg 存入 [memory],并設置 ZF=1
        ; 不等 → 將 [memory] 加載到 EAX,并設置 ZF=0
  1. 總線鎖定(Bus Locking)

    • LOCK?前綴使 CPU 在執行期間鎖定內存總線

    • 阻止其他核心/CPU 訪問同一內存區域

注意:CPU 在成功將緩存行標記為“獨占”(Exclusive)狀態之前,必須確保它擁有該緩存行的最新數據副本。如果它發現數據不是最新的,它就無法成功獲得獨占狀態,操作會失敗或需要重試。

  1. 緩存一致性協議(如 MESI)

    • 現代 CPU 通過緩存鎖定代替總線鎖定

    • 當 CPU 檢測到?CMPXCHG?操作時:

      • 將緩存行標記為"獨占"狀態

      • 若其他核心嘗試修改,會使其緩存行失效

      • 確保只有一個核心能成功修改

  2. 內存順序模型支持

    • 指令隱含內存屏障(Memory Barrier)

    • 保證操作前后的內存可見性順序

六、Volatile

6.1 字節碼

代碼示例:

public class Test {volatile int v;
}

字節碼:Field access_flags: 0x0040 (ACC_VOLATILE)

對于?v = 42(volatile 寫),字節碼只有一條簡單指令:

java

putfield #4  // 將值 42 寫入字段 v(#4 是常量池索引)

關鍵點

  • 字節碼?沒有顯式的屏障指令

  • JVM 通過字段的?ACC_VOLATILE?標志識別需要特殊處理

6.2 屏障類型

JVM 通過內存屏障實現?volatile?的語義,屏障類型如下:

屏障類型作用
LoadLoad確保當前讀操作在后續讀操作之前完成。
StoreStore確保當前寫操作在后續寫操作之前完成(刷新到主內存)。
LoadStore確保當前讀操作在后續寫操作之前完成。
StoreLoad全能屏障:確保當前寫操作對所有處理器可見后才執行后續讀/寫操作。

volatile?讀寫操作的屏障插入規則:

  1. volatile?寫操作

    • 寫操作前:StoreStore?屏障(防止與前面的普通寫重排序)。

    • 寫操作后:StoreLoad?屏障(防止與后面的?volatile?讀/寫重排序)。

    java

    StoreStore Barrier
    v = 42;  // volatile 寫
    StoreLoad Barrier
  2. volatile?讀操作

    • 讀操作前:LoadLoad?+?LoadStore?屏障(防止與后續操作重排序)。

    • 讀操作后:無額外屏障(某些架構下合并到前面)。

    java

    int tmp = v;  // volatile 讀
    LoadLoad Barrier + LoadStore Barrier

具體對應的CPU指令不寫了,過程有點復雜,大概率了解一下吧

屏障類型較多,理解起來也有點費勁,我直接說結果,即有了這些屏障后,程序的運行結果是怎樣的

6.3?volatile修飾的寫操作

// 第一階段:準備操作(普通讀寫)
普通操作1
普通操作2
...
普通操作N// StoreStore 屏障(隱形防線)
volatile 寫 = 新值;  // 關鍵操作// StoreLoad 屏障(隱形防線)
后續操作1
后續操作2
...
后續操作M

1.?屏障前的保證(前置防護)

  • ??所有 volatile 之前的操作都已計算完成
    (包括普通讀/寫、方法調用等)

  • ??所有普通寫操作結果全局可見
    (通過 StoreStore 屏障保證)

  • ??絕不允許 volatile 之后的操作重排到前面
    (StoreLoad 屏障阻止后續任何操作前移)

2.?屏障后的保證(后置防護)

  • ??volatile 寫本身全局可見
    (通過 StoreLoad 屏障強制刷新)

  • ??絕不允許 volatile 之前的操作重排到后面
    (StoreStore 屏障阻止前面操作后移)

6.4?volatile修飾的讀操作

// 前置操作區(可能被重排到此)
普通操作A
普通操作B

// 墻入口
int tmp = v;? // volatile讀
// 隱形的雙屏障防線
LoadLoad Barrier
LoadStore Barrier

// 后置保護區
后續操作1
后續操作2

1.?墻的入口(讀操作本身)

  • ??強制獲取最新值
    使當前 CPU 緩存失效,從主內存加載最新值

  • ???不限制前面操作
    允許墻前的普通操作重排到墻后(與寫操作關鍵區別!)

6.5 作用總結

一、volatile?寫操作的作用

當線程執行?volatile?寫操作(如?volatileVar = 42;)時:

  1. 可見性保證

    • ? 確保該寫操作完成后,所有線程都能立即看到這個新值。

    • 🔧 底層機制:強制刷新 CPU 寫緩沖區,將新值同步到主內存,并通過緩存一致性協議(如 MESI)使其他 CPU 的緩存副本失效。

  2. 有序性保證

    • ? 禁止指令重排序:

      • 禁止將?volatile?寫之前的任何操作?重排序到寫操作之后。

      • 禁止將?volatile?寫之后的讀操作?重排序到寫操作之前。

    • 🔧 底層機制:插入?StoreStore?+?StoreLoad?內存屏障(如 x86 的?lock?指令)。


二、volatile?讀操作的作用

當線程執行?volatile?讀操作(如?int val = volatileVar;)時:

  1. 可見性保證

    • ? 確保讀取到的是最新值(可能是其他線程剛寫入的值)。

    • 🔧 底層機制:強制從主內存或其他 CPU 重新加載數據(跳過本地可能過期的緩存)。

  2. 有序性保證

    • ? 禁止指令重排序:

      • 禁止將?volatile?讀之后的任何操作?重排序到讀操作之前。

      • 禁止將?volatile?讀之前的寫操作?重排序到讀操作之后。

    • 🔧 底層機制:插入?LoadLoad?+?LoadStore?內存屏障(如 ARM 的?dmb?指令)。

6.6 x86 CPU實現

以下是幾種常見架構上,為了實現?volatile?語義,HotSpot JVM 通常使用的屏障和對應的 CPU 指令:

    1. x86/x86-64:

      • volatile?寫:

        • 屏障要求:?StoreStore 屏障 + StoreLoad 屏障

        • 實際指令:?lock addl $0x0, (%rsp)?(或其他類似指令)

          • lock?前綴:這是 x86 上實現強內存屏障效果的關鍵。它鎖定內存總線(或使用緩存一致性協議如 MESI),確保該指令的操作具有原子性,并隱式包含了 StoreLoad 屏障。它還會刷新寫緩沖區。

          • addl $0x0, (%rsp):這是一個對棧頂指針?%rsp?指向的內存地址加 0 的空操作。它本身不改變數據,目的是提供一個讓?lock?前綴作用的目標指令。

          • 為什么不需要顯式 StoreStore??x86 架構的 TSO (Total Store Order) 內存模型本身保證了寫操作(包括非 volatile 寫)不會重排序。因此,在?volatile?寫之前插入的 StoreStore 屏障在 x86 上通常是?空操作nop)。

      • volatile?讀:

        • 屏障要求:?LoadLoad 屏障 + LoadStore 屏障

        • 實際指令:?通常沒有顯式的屏障指令

          • 原因:?x86 的 TSO 模型天然保證了:

            • LoadLoad 不會重排序(后面的讀能看到前面的讀的結果)。

            • LoadStore 不會重排序(后面的寫不會重排序到前面的讀之前)。

          • 因此,對于?volatile?讀,JVM 在 x86 上通常只需要生成普通的?mov?指令來加載值,而不需要插入任何顯式的屏障指令volatile?讀本身的內存語義(如緩存行失效)由 CPU 的緩存一致性協議(MESI)自動處理。

      • 總結 (x86):

        • volatile?寫:lock addl $0x0, (%rsp)?(或等效指令) -> 主要提供?StoreLoad 屏障

        • volatile?讀:普通的?mov?指令 ->?屏障是空操作

Q:那lock addl $0x0, (%rsp) 指令和 修改 volatile值的mov指令 不是原子的,豈不是會造成值修改了,但是不可見的情況?

lock addl $0x0, (%rsp)?的核心作用是:

  1. 排空寫緩沖區
    強制當前 CPU 的寫緩沖區中所有數據(包括之前的?mov立即刷到緩存/內存

  2. 使其他 CPU 的緩存失效
    通過緩存一致性協議(MESI)廣播,使其他 CPU 中該 volatile 變量的緩存行失效。

  3. StoreLoad 屏障
    確保后續讀操作必須重新從主存加載最新值。

假設?mov?執行后、lock?執行前發生中斷:

x86asm

mov [var], eax     ; 值進入寫緩沖區
; <-- 此處發生中斷(寫緩沖區未刷新)
lock addl $0x0, (%rsp) ; 中斷返回后執行
  • 問題:其他 CPU 在中斷期間可能讀到舊值。

  • 解答:x86 的中斷處理機制保證:

    1. 中斷返回前會隱式排空寫緩沖區(類似?sfence)。

    2. 中斷結束后執行?lock?指令,再次強制刷新并失效化緩存。

  • 結果:最終仍保證可見性(可能略有延遲,但符合 Java 語義)。

七、Reentrantlock實現原理

7.1 AQS

Reentrantlock基于AQS實現,AQS介紹

  1. 狀態變量 (state):

    • 一個?volatile int?類型的變量,表示鎖的狀態。

    • 對于?ReentrantLock

      • state = 0: 鎖未被任何線程持有。

      • state > 0: 鎖已被某個線程持有。數值表示該線程重入鎖的次數(同一個線程多次獲取鎖)。

  2. CLH 隊列 (FIFO 線程等待隊列):

    • 一個雙向鏈表(或變體)實現的等待隊列。

    • 當多個線程競爭鎖失敗時,它們會被構造成?Node?節點,并加入到這個隊列尾部排隊等待。

    • 隊列中的線程會以 FIFO(先進先出)的順序被喚醒(公平模式下嚴格 FIFO,非公平模式下可能插隊)。

7.2 lock

    • 嘗試獲取 (tryAcquire):

      • 線程首先嘗試通過 CAS 操作將?state?從 0 改為 1。

      • 成功 (state 原來是 0):

        • 設置當前線程為鎖的獨占所有者 (exclusiveOwnerThread)。

        • 返回 true,獲取鎖成功。

      • 失敗 (state > 0):

        • 檢查當前線程是否已經是鎖的持有者 (exclusiveOwnerThread == currentThread)。

        • 如果是,則將?state?加 1(重入計數增加),返回 true。

        • 如果不是,返回 false。

    • 加入隊列等待 (acquireQueued):

      • 如果?tryAcquire?失敗(返回 false),線程會將自己包裝成一個?Node?節點。

      • 使用 CAS 操作將節點安全地加入到 CLH 隊列的尾部。

      • 進入自旋或阻塞狀態:

        • 檢查自己是否是隊列中第一個有效等待節點(頭節點的后繼)。

        • 如果是,再次嘗試?tryAcquire?(非公平鎖總是嘗試一次;公平鎖嚴格排隊)。

        • 如果還不是第一個節點或嘗試失敗:

          • 檢查前驅節點的狀態,判斷是否需要阻塞自己。

          • 調用?LockSupport.park(this)?將當前線程掛起(阻塞)。

        • 線程被喚醒(通常是前驅節點釋放鎖時喚醒它)后,會再次嘗試獲取鎖。

7.3 unlock

  • 嘗試釋放 (tryRelease):

    • 檢查當前線程是否是鎖的持有者(exclusiveOwnerThread == currentThread),否則拋出?IllegalMonitorStateException

    • 將?state?減 1(表示減少一次重入計數)。

    • 如果?state?減到 0:

      • 將?exclusiveOwnerThread?設置為?null,表示鎖完全釋放。

      • 返回 true。

    • 如果?state?仍然大于 0(說明還有重入),返回 false。

  • 喚醒后繼 (unparkSuccessor):

    • 如果?tryRelease?返回 true(鎖完全釋放),則找到 CLH 隊列中第一個狀態正常的等待節點(通常是頭節點的后繼)。

    • 調用?LockSupport.unpark(s.thread)?喚醒該節點對應的線程,使其有機會去競爭鎖。

相關代碼:

7.4 CLH隊列

前面加鎖和解鎖過程都使用了到了CLH隊列,下面具體介紹一下什么是CLH隊列,以及在Reentrantlock中做了哪些優化

  • 核心思想:?一個隱式鏈接的 FIFO 隊列,用于管理等待鎖(或共享資源)的線程。每個等待線程被封裝成一個節點(Node)

  • 關鍵機制:?每個節點通過一個?volatile?狀態字段(通常是?waitStatus?來輪詢(spin)?其前驅節點(predecessor node)?的狀態。

  • 核心操作:

    • 入隊(獲取鎖失敗時):?新線程嘗試獲取鎖失敗后,會創建一個新節點,通過?CAS?(Compare-And-Swap)?操作將自身設置為新的?tail(尾指針),同時記錄下它入隊時看到的尾節點作為其前驅節點(prev)

    • 等待(自旋輪詢前驅):?線程在一個循環中不斷檢查其前驅節點的狀態標志(waitStatus)。如果前驅節點的狀態表明它已經釋放了鎖(或即將釋放),那么當前線程就有資格嘗試獲取鎖。關鍵點:線程只關心它前面的那個節點(前驅)的狀態。

    • 出隊(獲取鎖成功):?當線程檢測到前驅節點釋放了鎖(狀態變為?SIGNAL?或類似),它成功獲取鎖,并成為新的隊列頭(head)。原頭節點通常被移除或成為虛擬頭節點。

    • 釋放鎖(通知后繼):?當持有鎖的線程(隊列頭節點)釋放鎖時,它會檢查其后繼節點(next)的狀態。如果后繼節點在等待(狀態為?SIGNAL),它會修改自身的狀態(例如,設置?waitStatus = 0?或直接清除狀態)或直接設置后繼節點的狀態(在 AQS 變體中通常是設置頭節點狀態為?SIGNAL?后由后繼輪詢),這個狀態變化會被其后繼節點(正在輪詢前驅狀態)立即感知volatile?保證可見性),從而喚醒后繼節點去嘗試獲取鎖。

  • 原始 CLH 特點(純自旋):

    • FIFO 公平性。

    • 線程在 CPU 上忙等(自旋)其前驅的狀態變化。

    • 鎖釋放僅需修改一個?volatile?變量(自身狀態),通知是無鎖且高效的。

    • 避免了“驚群效應”(只喚醒一個后繼)。

AQS中如何使用?

  1. 顯式雙向鏈表:

    • AQS 節點不僅維護指向前驅節點(prev?的指針(用于輪詢狀態和取消時移除),還維護指向后繼節點(next?的指針。這不是原始 CLH 必需的(原始 CLH 通常只有隱式前驅鏈),但大大簡化了節點取消(Cancellation)(中斷、超時)時的鏈表操作。

  2. 虛擬頭節點(Dummy Head):

    • AQS 隊列初始化時通常會創建一個不關聯任何線程的虛擬頭節點。第一個真正等待的線程節點會成為虛擬頭節點的后繼。這使得隊列操作(如判斷是否有等待線程、喚醒后繼)邏輯更統一,避免邊界條件判斷。

  3. 狀態整合(waitStatus):

    • AQS 節點的?waitStatus?字段承載了比原始 CLH 狀態標志更豐富的含義:

      • SIGNAL(-1):?最重要的狀態。?表示該節點的后繼節點需要被喚醒(即,后繼節點在等待)。當前節點釋放鎖或取消時,必須喚醒其后繼節點。節點在阻塞自己之前,通常會將其前驅節點的?waitStatus?設置為?SIGNAL(通過?CAS),這樣前驅節點就知道“我后面有人等著呢,你釋放時記得叫我”。

      • CANCELLED(1): 節點關聯的線程已取消等待(如超時或中斷)。需要從隊列中安全移除。

      • CONDITION(-2): 節點當前在條件隊列ConditionObject)中等待,而不是在主同步隊列中。

      • PROPAGATE(-3):?僅用于共享模式。?表示下一次?acquireShared?操作應該無條件傳播(表示后續節點也可能可以獲取共享資源)。

      • 0: 初始狀態,或表示節點不處于上述任何特殊狀態。

  4. 阻塞代替自旋(關鍵優化):

    • 這是 AQS CLH 變體最核心的改進!?原始 CLH 是純自旋(忙等),消耗 CPU。

    • AQS 中,線程不會持續自旋輪詢前驅狀態。其流程是:

      • a) 嘗試獲取鎖。

      • b) 失敗,創建節點并入隊(設置?tail,鏈接?prev)。

      • c)?快速自旋檢查前驅狀態:?在一個循環中快速檢查幾次:

        • 如果前驅是頭節點(說明快輪到自己了),再次嘗試獲取鎖(防止不必要的阻塞)。

        • 如果前驅節點的?waitStatus == SIGNAL,說明前驅已設置好“釋放時喚醒我”的標志,安全地阻塞自己(調用?LockSupport.park())。

        • 如果前驅狀態是?CANCELLED,則跳過該前驅,繼續找更前面的有效前驅。

        • 如果前驅狀態正常但不是?SIGNAL,則嘗試用?CAS?將前驅的?waitStatus?設置為?SIGNAL(告訴它“你釋放時記得叫我”)。

      • d) 如果步驟 c 中的檢查表明可以安全阻塞了(前驅是?SIGNAL),則調用?park()?掛起當前線程。

    • 喚醒:?當持有鎖的線程(頭節點)釋放鎖時,它會檢查頭節點的?waitStatus。如果是?SIGNAL(通常都會是,因為后繼在阻塞前設置了它),它會找到其后繼節點(next),并調用?LockSupport.unpark(successor.thread)?喚醒其后繼線程。被喚醒的線程從?park()?處返回,回到步驟 c 的循環中,再次嘗試獲取鎖。

  5. 入隊(enq)與設置前驅狀態(shouldParkAfterFailedAcquire):

    • 入隊操作 (enq) 使用?CAS?保證線程安全地更新?tail

    • shouldParkAfterFailedAcquire?方法實現了步驟 c 中的邏輯:檢查/清理前驅狀態,確保前驅是有效的且?waitStatus == SIGNAL,然后才決定調用?park()

7.5 與?synchronized?關鍵字的對比

  • 相似點:都是可重入互斥鎖。

  • ReentrantLock 優勢

    • 靈活性:支持公平鎖/非公平鎖選擇、可中斷鎖等待 (lockInterruptibly)、超時鎖等待 (tryLock(timeout))、多條件變量 (newCondition)。

    • API 化:顯式的?lock()?和?unlock()?操作,控制更精細。

    • 性能:在高度競爭的場景下,現代 JVM 的?synchronized?優化(鎖升級)已經非常高效,兩者性能差距不大。但在某些特定場景(如大量讀少量寫),ReentrantLock 結合?ReadWriteLock?可能更優。

  • synchronized 優勢

    • 簡潔性:語法簡單,由 JVM 自動管理鎖的獲取和釋放(在 synchronized 代碼塊結束時釋放),不易出錯(避免忘記?unlock)。

    • JVM 優化:JVM 深度優化(偏向鎖、輕量級鎖、鎖消除、鎖粗化等)。

下面說一下reentrantlock實現的

7.5 可中斷鎖和超時鎖

可中斷鎖:

源碼:

private void doAcquireInterruptibly(int arg) throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE); // 將當前線程包裝為獨占模式Node加入隊列尾部boolean failed = true;try {for (;;) { // 自旋等待final Node p = node.predecessor(); // 獲取前驅節點if (p == head && tryAcquire(arg)) { // 如果前驅是頭節點(輪到我了)且嘗試獲取鎖成功setHead(node); // 將自己設為新的頭節點(出隊)p.next = null; // help GCfailed = false;return; // 成功獲取鎖,退出方法}if (shouldParkAfterFailedAcquire(p, node) && // 檢查是否應該阻塞(前驅狀態正常)parkAndCheckInterrupt()) // 4. 真正阻塞線程,并在此處檢查中斷!throw new InterruptedException(); // 5. 如果阻塞中被中斷,拋出異常!}} finally {if (failed)cancelAcquire(node); // 6. 如果最終失敗(如因中斷),取消節點獲取狀態}
}private final boolean parkAndCheckInterrupt() {LockSupport.park(this); // 使用LockSupport.park()掛起當前線程return Thread.interrupted(); // 線程被喚醒后,立即檢查并清除中斷標志,返回是否因中斷被喚醒
}

當線程判斷需要阻塞時(通過?shouldParkAfterFailedAcquire),調用?LockSupport.park(this)?掛起當前線程

  • 線程可能被以下三種方式喚醒:

    1. 持有鎖的線程釋放鎖,并喚醒隊列中的后繼節點(unparkSuccessor)。

    2. 其他線程調用了該線程的?interrupt()?方法。

    3. 虛假喚醒(spurious wakeup)。

  • 線程被喚醒后,第一件事就是調用?Thread.interrupted()。這個方法做兩件事:

    • 返回線程當前的中斷狀態(true?表示被中斷過)。

    • 清除線程的中斷狀態(設為?false)。

超時鎖:

final boolean doAcquireNanos(int arg, long nanosTimeout) {if (nanosTimeout <= 0L) return false; // 時間已到,直接失敗final long deadline = System.nanoTime() + nanosTimeout;final Node node = addWaiter(Node.EXCLUSIVE); // 加入隊列boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) { // 如果是隊首則嘗試獲取鎖setHead(node);p.next = null; // 幫助GCfailed = false;return true;}nanosTimeout = deadline - System.nanoTime();if (nanosTimeout <= 0L) return false; // 超時檢查if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold) { // 大于閾值才掛起LockSupport.parkNanos(this, nanosTimeout); // 關鍵:掛起指定時間}if (Thread.interrupted()) // 響應中斷throw new InterruptedException();}} finally {if (failed) cancelAcquire(node); // 失敗則取消節點}
}

其實可以看到 無論是可中斷鎖還是超時鎖,它們都使用了LockSupport這個對象來加鎖解鎖,沒有采用synchronized這種操作,而LockSupport支持鎖中斷,支持鎖超時機制,所以這就是reetrantlock能實現這些多功能鎖的原因了

7.6 Condition

基本使用:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();// 等待方
lock.lock();
try {while (!conditionSatisfied) {  // 循環檢查條件condition.await();         // 釋放鎖并等待}// 執行條件滿足后的操作
} finally {lock.unlock();
}// 通知方
lock.lock();
try {// 改變條件condition.signal();  // 或 signalAll()
} finally {lock.unlock();
}

實現原理簡要介紹:

Condition?的核心實現基于?AQS(AbstractQueuedSynchronizer),講解Condition時會涉及兩個隊列,一個是同步隊列,就是AQS存儲線程的隊列,一個是條件隊列,指的是condition對象對應的隊列,以下是關鍵原理:

  1. 等待隊列(條件隊列)

    • 每個?Condition?對象內部維護一個?FIFO 等待隊列(單向鏈表)。

    • 當線程調用?await()?時,會釋放鎖、修改對應lock鎖的狀態、并進入條件隊列等待。

    • 隊列節點類型與 AQS 同步隊列相同(Node?類)。

  2. 節點轉移機制

    • 調用?signal()?時,將條件隊列的頭節點移動到 AQS 同步隊列尾部。正好對應上了上面代碼的isOnSyncQueue

    • 移動到同步隊列的節點會嘗試獲取鎖,成功后從?await()?返回。

  3. 喚醒與阻塞控制

    • await():釋放鎖 → 阻塞線程 → 加入條件隊列。

    • signal():將條件隊列的首節點移入同步隊列,喚醒線程。

    • signalAll():移動條件隊列所有節點到同步隊列。

  4. 避免虛假喚醒
    通過循環檢查條件(while (condition) await())確保線程被喚醒時條件真正滿足。

八、線程阻塞或喚醒時操作系統的動作

1.?用戶態嘗試獲取鎖

  • 步驟

    1. 線程在用戶態通過 CAS(Compare-And-Swap)自旋嘗試獲取鎖(例如?synchronized?的偏向鎖/輕量級鎖)。

    2. 若鎖未被競爭(無沖突),直接獲取成功,全程在用戶態完成

  • 關鍵點
    此時無系統調用,不涉及內核態切換,性能極高。

2.?競爭失敗:進入自適應自旋

  • 步驟

    1. 當 CAS 失敗(鎖被其他線程占用),JVM 啟動自適應自旋(根據歷史成功率動態調整自旋次數)。

    2. 線程在用戶態循環重試,仍不切內核態

  • 關鍵點
    自旋避免了立即陷入內核,但消耗 CPU 時間。

3.?自旋失敗:真正阻塞(切內核態)

  • 步驟

    1. 自旋仍無法獲取鎖時,JVM 調用底層操作系統的線程阻塞原語(如 Linux 的?futex())。

    2. 線程狀態從?RUNNABLE?變為?BLOCKED

    3. 觸發系統調用(如?futex)→ 從用戶態陷入內核態

  • 關鍵點
    此處是用戶態切內核態的核心節點!

4.?內核態操作

  • 內核完成以下動作

    1. 保存線程上下文(寄存器、程序計數器等)。

    2. 將線程移入鎖的等待隊列(由內核管理)。

    3. 觸發線程調度:從就緒隊列選擇新線程運行。

    4. 切換 CPU 上下文到新線程。

  • 關鍵點
    所有操作均在內核態完成,需要特權指令。

5.?喚醒階段(再次切內核態)

  • 當鎖釋放時

    1. 持有鎖的線程調用?unlock(),觸發 JVM 的喚醒機制。

    2. JVM 通過?futex_wake()?系統調用陷入內核態

    3. 內核從等待隊列中移出一個/多個線程,標記為就緒狀態。

    4. 線程重新參與調度,下次被 CPU 選中時恢復執行。

  • 關鍵點
    喚醒操作同樣需切內核態,恢復線程時需切換回用戶態。

一點操作系統知識,大概了解一下:

上面使用到了futex,操作系統將所有等待同一 futex(相同 uaddr)的線程被組織在同一個等待隊列中

1.?uaddr 的本質

  • uaddr?是用戶空間的一個內存地址(32位整數),通常指向:

    • 鎖狀態變量(如 Java 對象頭中的 MarkWord)

    • 條件變量標志位

    • 信號量計數器

  • 關鍵特性
    相同 uaddr 值 → 代表同一同步資源(如同一把鎖)

  • 一對一映射
    每個唯一的 uaddr 值對應?一個且僅一個?內核等待隊列

  • 自動創建
    當首個線程在某個 uaddr 上調用?futex_wait()?時,內核自動創建隊列

  • 自動銷毀
    當隊列為空(無等待線程)時,內核自動銷毀隊列

// 每個 futex 等待隊列
struct futex_queue {struct list_head threads; // 線程鏈表(FIFO)u32 *uaddr;               // 綁定的用戶態地址atomic_t waiters;         // 等待線程計數
};

九、原子性、有序性、可見性的保證

特性保障方法
原子性synchronizedReentrantLock、原子類(AtomicInteger?等)、CAS
可見性volatilesynchronized/鎖、final、CAS
有序性volatile(禁止重排序)、synchronized/鎖(內存屏障)、fina、CAS

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

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

相關文章

線程池111

線程池框圖C語言線程池詳解&#xff1a;從基礎到實現通俗理解線程池想象你開了一家快遞站&#xff0c;每天要處理很多包裹派送&#xff1a;?沒有線程池?&#xff1a;每來一個包裹就雇一個新快遞員&#xff0c;送完就解雇問題&#xff1a;頻繁招聘解雇成本高&#xff08;線程創…

Qt-Advanced-Docking-System

直譯一下 &#xff1a; 先進的停靠系統 github: mfreiholz/Qt-Advanced-Docking-System: Advanced Docking System for Qt 這是這個項目的起源 這個最后一次更新&#xff1a; githubuser0xFFFF/Qt-Advanced-Docking-System: Advanced Docking System for Qt 這是另一個人復刻…

湖南(源點咨詢)市場調研 如何在行業研究中快速有效介入 中篇

我們接著起頭篇來說邁克爾波特認為一個行業內存在著五種基本競爭力量&#xff0c;即潛在入侵者、替代產品、供方、需方以及行業內現有競爭者。如附圖&#xff1a;即&#xff1a;同行業內現有競爭者的競爭能力、潛在競爭者進入的能力、替代品的替代能力、供應商的討價還價能力、…

【無標題】消息隊列(Message Queue)是一種**進程間通信(IPC)機制

消息隊列&#xff08;Message Queue&#xff09;是一種進程間通信&#xff08;IPC&#xff09;機制&#xff0c;它允許進程通過在隊列中添加和讀取消息來交換數據。與管道&#xff08;命名/匿名&#xff09;相比&#xff0c;消息隊列具有結構化消息、異步通信和消息持久化等特點…

mac中多版本JDK配置和切換

下載 從jdk官網下載即可&#xff0c;找到自己要用的版本。 官網&#xff1a;https://www.oracle.com/java/technologies/downloads/#jdk21-mac 我這里下載的jdk1.8和21。 根據自己芯片下載&#xff0c;一般都是m芯片。下載好后&#xff0c;點擊&#xff0c;一直下一步就行&…

【JVM】流程匯總

【JVM】流程匯總【一】編譯過程和內存分布【1】案例程序&#xff1a;簡單的 Java 類【2】Java 編譯過程&#xff1a;從.java到.class&#xff08;1&#xff09;編譯命令&#xff08;2&#xff09;編譯結果&#xff08;3&#xff09;字節碼的作用【3】Java 運行過程&#xff1a;…

專業MP3瘦身工具WinMP3Shrink 1.1,綠色單文件,極速壓縮

[軟件名稱]: 專業MP3瘦身工具WinMP3Shrink 1.1 [軟件大小]: 1.1 MB [軟件大小]: 夸克網盤 | 百度網盤 軟件介紹 WinMP3Shrink 是一款免費的 MP3 壓縮軟件&#xff0c;能夠有效減少 MP3 文件的體積&#xff0c;同時還能增強音質。即使不重新編碼&#xff0c;通過移除保留空間…

LeetCode 每日一題 2025/8/4-2025/8/10

記錄了初步解題思路 以及本地實現代碼&#xff1b;并不一定為最優 也希望大家能一起探討 一起進步 目錄8/4 904. 水果成籃8/5 3477. 水果成籃 II8/6 3479. 水果成籃 III8/7 3363. 最多可收集的水果數目8/8 808. 分湯8/9 231. 2 的冪8/10 869. 重新排序得到 2 的冪8/4 904. 水果…

Python爬蟲實戰:研究Ruia框架,構建博客園文章采集系統

1. 引言 1.1 研究背景與意義 在數字化時代,數據已成為驅動科技創新與產業升級的核心生產要素。互聯網作為全球最大的信息載體,蘊含著億級結構化、半結構化與非結構化數據,這些數據在商業決策、學術研究、公共服務等領域具有不可替代的價值。網絡爬蟲技術作為自動獲取網絡公…

Office安裝使用?借助Ohook開源工具?【圖文詳解】微軟Office產品

一、問題背景 很多用戶在使用 Office 軟件一段時間后&#xff0c;會遇到以下問題。 二、解決方案 Ohook 是 Office 獨有的可用方式&#xff0c;源自 GitHub 上的開源項目&#xff0c;代碼開源&#xff08;開源地址&#xff1a;https://github.com/asdcorp/ohook&#xff09;。 …

LeetCode簡單題 - 學習

力扣題庫 - 簡單題 - 僅記錄學習 來源地址&#xff1a; 力扣 (LeetCode) 全球極客摯愛的技術成長平臺 1. 兩數之和 給定一個整數數組 nums 和一個整數目標值 target&#xff0c;請你在該數組中找出 和為目標值 target 的那 兩個 整數&#xff0c;并返回它們的數組下標。 你…

Android Camera 打開和拍照APK源碼

完整下載路徑: 【免費】AndroidcameraAPK完整源碼(包括打開攝像頭和拍照保存功能)Android10驗證可完整運行資源-CSDN下載 效果: 源碼: package com.example.mycamera;import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appco…

【系統分析師】軟件需求工程——第11章學習筆記(上)

軟件需求工程是包括創建和維護軟件需求文檔所必需的一切活動的過程。可分為兩大工作&#xff1a;需求開發需求獲取需求分析需求定義&#xff08;編寫需求規格說明書&#xff09;需求驗證需求管理定義需求基線處理需求變更需求跟蹤在需求開發階段需要確定軟件所期望的用戶類型&a…

機器學習第七課之支持向量機SVM

目錄 簡介&#xff1a; 一、什么是支持向量機 二、如何選取最佳的超平面 1.超平面方程 (優化目標) 2.如何尋找最優的超平面 3.舉例分析 4.軟間隔?編輯 三、核函數 1舉例 2常用核函數 3.多項式核函數 4.高斯核函數: 四、svm的優缺點 五、支持向量機的API 六、案例…

P3232 [HNOI2013] 游走,solution

原題&#xff1a; link&#xff0c;點擊這里喵。 題意&#xff1a; 給定一個 nnn 個點 mmm 條邊的無向連通圖&#xff0c;圖無重邊和自環&#xff0c;頂點從 111 編號到 nnn&#xff0c;邊從 111 編號到 mmm。 小 Z 在該圖上進行隨機游走&#xff0c;初始時小 Z 在 111 號頂…

Docker容器部署discuz論壇與線上商城

準備 關閉防火墻&#xff0c;上下文[rootdocker ~]# systemctl disable --now firewalld[rootdocker ~]# setenforce 0下載應用yum remove runc -y ### rocky8才需要yum install -y yum-utils yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/cento…

Linux入門指南:26個基礎命令全解析

目錄 一.基礎概念與入門 1.Linux操作系統簡介 2.終端與shell的基本概念 3.命令行界面的優勢 二.基礎指令 1.whoami ?2.useradd/userdel/passwd ?3.pwd ?4.ls ?5.cd 6.touch 7.mkdir 8.tree 9.rmdir/rm 10.man 11.cp 12.mv 13.cat 14.le…

【后端】Java 8 特性 `User::getId` 語法(方法引用)介紹

文章目錄核心概念解析&#xff1a;方法引用的四種類型&#xff1a;關鍵特性&#xff1a;使用場景推薦&#xff1a;何時避免使用&#xff1a;性能說明&#xff1a;在 Java 中&#xff0c; User::getId 是一種稱為 方法引用&#xff08;Method Reference&#xff09; 的語法糖&a…

基于BP與CNN的圖像分類模型構建、超參數優化及性能對比研究?

一、實驗目的實驗目標構建基于神經網絡模型的數據分析與模式識別框架&#xff0c;探明神經網絡在大數據分析中的意義。實驗任務構建基于深度 BP 神經網絡與卷積神經網絡的數據分析與模式識別框架&#xff0c;將數據集 MNIST 與 CIFAR-10 分別在兩種模型中訓練&#xff0c;并比較…

HarmonyOS應用開發-低代碼開發登錄頁面(超詳細)

本篇文章我來手把手教大家做一個HarmonyOS 應用的登錄頁面&#xff0c;逐步講解&#xff0c;非常細致&#xff0c;百分百能學會&#xff0c;并提供全部源碼。頁面使用 DevEco Studio 的低代碼開發。 通過本文的實踐經驗&#xff0c;我想告訴大家&#xff0c; HarmonyOS 應用開發…