博客 / 詳情

返回

Java 鎖進化論:synchronized 的底層原理與鎖優化技術詳解

在多線程編程中,synchronized是 Java 中最基礎也最重要的同步機制之一。雖然它在 JDK 早期版本中因性能問題被詬病,但隨着 JDK 1.6 引入的鎖優化技術,它已經成為兼具性能和易用性的同步方案。本文將深入剖析 synchronized 的底層原理、鎖升級過程以及 JVM 對它的各種優化措施。

一、synchronized 的三種使用形式

在深入原理前,先回顧一下 synchronized 的三種基本使用形式:

  1. 修飾實例方法:鎖定當前對象實例
  2. 修飾靜態方法:鎖定當前類的 Class 對象
  3. 修飾代碼塊:鎖定指定的對象
public class SynchronizedDemo {
    // 1. 修飾實例方法(鎖是當前實例對象)
    public synchronized void instanceMethod() {
        // 臨界區代碼
        System.out.println("實例方法同步");
    }

    // 2. 修飾靜態方法(鎖是當前類的Class對象)
    public static synchronized void staticMethod() {
        // 臨界區代碼
        System.out.println("靜態方法同步");
    }

    // 3. 修飾代碼塊(鎖是括號裏指定的對象)
    public void blockMethod() {
        synchronized(this) {
            // 臨界區代碼
            System.out.println("代碼塊同步(this)");
        }

        synchronized(SynchronizedDemo.class) {
            // 臨界區代碼
            System.out.println("代碼塊同步(類對象)");
        }

        Object lock = new Object();
        synchronized(lock) {
            // 臨界區代碼
            System.out.println("代碼塊同步(任意對象)");
        }
    }
}

二、synchronized 的底層實現原理

要真正理解 synchronized,必須從 JVM 層面看它是如何實現的。

1. 字節碼層面

當 synchronized 修飾代碼塊時,JVM 會在同步塊的前後分別插入monitorentermonitorexit指令:

public void syncBlock() {
    synchronized(this) {
        System.out.println("同步塊");
    }
}

使用javap -c命令反編譯上述方法,可以看到:

public void syncBlock();
    Code:
      0: aload_0
      1: dup
      2: astore_1
      3: monitorenter    // 進入同步塊
      4: getstatic       #2  // 同步塊內代碼
      ...
     13: aload_1
     14: monitorexit     // 退出同步塊
     ...

當 synchronized 修飾方法時,JVM 不會使用 monitorenter 和 monitorexit 指令,而是在方法的訪問標誌中增加 ACC_SYNCHRONIZED 標誌:

method_info {
    u2 access_flags;     // 訪問標誌,其中包含ACC_SYNCHRONIZED
    u2 name_index;
    u2 descriptor_index;
    ...
}

2. Monitor 機制

無論哪種形式,synchronized 的底層都依賴 Monitor(監視器)機制實現。每個對象都有一個關聯的 Monitor:

graph TD
    A[Java對象] --> B[對象頭]
    A --> C[實例數據]
    A --> D[對齊填充]
    B --> E[Mark Word]
    B --> F[類元數據指針]
    E --> G[鎖信息/GC信息/HashCode...]

Mark Word 記錄了對象的狀態,包括鎖信息、垃圾回收信息、hashCode 等。

3. Monitor 核心數據結構

Monitor 本質上是一個同步工具:

graph LR
    A[Monitor對象] --> B[Owner線程]
    A --> C[Entry Set等待隊列]
    A --> D[Wait Set等待隊列]
    A --> E[計數器]
  • Owner:持有鎖的線程
  • Entry Set:等待獲取鎖的線程集合
  • Wait Set:調用 wait()方法後,線程進入此隊列
  • 計數器:記錄重入次數,實現 synchronized 的可重入性,當同一線程多次獲取同一鎖時,計數器累加,釋放鎖時遞減,類似於 ReentrantLock 中的 holdCount

Monitor 對象由 JVM 在堆中創建,重量級鎖狀態下,對象頭的 Mark Word 中存儲的是指向該 Monitor 對象的指針。

三、鎖的升級過程

JDK 1.6 引入了鎖優化,核心是鎖的升級過程:偏向鎖 → 輕量級鎖 → 重量級鎖

1. 偏向鎖

偏向鎖的核心思想:大多數情況下,鎖不存在競爭,同一個線程反覆獲取同一把鎖。

// 偏向鎖示例
public class BiasedLockDemo {
    public static void main(String[] args) throws Exception {
        // JVM默認啓用偏向鎖,但有4秒延遲
        // 可以通過-XX:BiasedLockingStartupDelay=0取消延遲
        // 等待偏向鎖機制激活
        Thread.sleep(5000);

        Object lock = new Object();

        // 同一個線程多次獲取鎖
        for (int i = 0; i < 5; i++) {
            synchronized (lock) {
                // 臨界區代碼
                System.out.println("偏向鎖生效中...");
            }
        }
    }
}

偏向鎖在 Mark Word 中記錄線程 ID 和 epoch 值,下次相同線程獲取鎖時,通過比對線程 ID 直接獲取鎖,無需 CAS 操作。Mark Word 中的標誌位為01,且偏向標記為1

偏向鎖撤銷的觸發條件包括:

  • 其他線程競爭該鎖
  • 調用對象的 hashCode()方法(偏向鎖沒有存儲 hashCode 的空間)
  • GC 過程中發現有偏向鎖
  • 顯式禁用偏向鎖(-XX:-UseBiasedLocking

2. 輕量級鎖

當有第二個線程嘗試獲取鎖時,偏向鎖升級為輕量級鎖。輕量級鎖通過 CAS(Compare and Swap)操作替代重量級鎖的互斥量操作。

graph TD
    A[輕量級鎖] --> B[線程棧幀中創建Displaced Mark Word]
    B --> C[CAS嘗試將對象頭Mark Word替換為指向棧中鎖記錄的指針]
    C --> D{替換成功?}
    D -->|是| E[獲取鎖成功]
    D -->|否| F[自旋等待/升級重量級鎖]

輕量級鎖的核心是:

  1. 線程在自己的棧幀中創建 Displaced Mark Word,存儲對象頭 Mark Word 的備份
  2. 通過 CAS 操作,將對象頭中的 Mark Word 替換為指向線程棧中鎖記錄的指針
  3. 若 CAS 成功,獲取輕量級鎖成功;若失敗,進入自旋等待或升級為重量級鎖

此時 Mark Word 中的標誌位為00

// 輕量級鎖示例
public class LightweightLockDemo {
    public static void main(String[] args) {
        final Object lock = new Object();

        // 創建兩個線程競爭鎖
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 1 acquired lock");
                try {
                    Thread.sleep(20); // 保持鎖一段時間
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 稍等片刻再啓動第二個線程
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 2 acquired lock");
            }
        }).start();
    }
}

3. 重量級鎖

當以下情況發生時,輕量級鎖會升級為重量級鎖:

  1. 自旋達到閾值(默認 10 次,可通過-XX:PreBlockSpin設置)
  2. 有多個線程同時競爭鎖,自旋不再是有效的等待方式
  3. 持有鎖的線程在自旋期間未釋放鎖(如執行時間較長的臨界區)

重量級鎖使用操作系統的互斥量(mutex)實現。進入重量級鎖狀態後,沒有獲得鎖的線程會被阻塞,直到持有鎖的線程釋放鎖。此時 Mark Word 中的標誌位為10,存儲的是指向 Monitor 對象的指針。

重量級鎖狀態下,Mark Word 中存儲的是指向堆中 Monitor 對象的指針。這個 Monitor 對象包含了 Owner、Entry Set、Wait Set 等結構,用於管理線程的阻塞和喚醒。

// 重量級鎖示例(多線程高競爭)
public class HeavyweightLockDemo {
    private static final int THREAD_COUNT = 20;
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[THREAD_COUNT];

        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    synchronized (HeavyweightLockDemo.class) {
                        counter++;
                    }
                }
            });
            threads[i].start();
        }

        // 等待所有線程執行完成
        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("最終計數: " + counter);
    }
}

4. 鎖狀態轉換圖

graph LR
    A[無鎖] -->|線程首次獲取| B[偏向鎖]
    B -->|其他線程競爭| C[輕量級鎖]
    B -->|調用hashCode| C
    B -->|GC過程| C
    B -->|禁用偏向鎖| C
    C -->|自旋達到閾值或競爭激烈或自旋未獲得鎖| D[重量級鎖]
    D -->|鎖釋放| A
    C -->|鎖釋放無競爭| A

四、JVM 對 synchronized 的優化

1. 鎖消除

JIT 編譯器在運行時通過逃逸分析,檢測到某些同步代碼不可能存在競爭(對象僅在方法內部使用,未逃逸到其他線程),會自動消除鎖操作。

public class LockEliminationDemo {
    public void method() {
        // StringBuffer是線程安全的,內部使用synchronized
        StringBuffer sb = new StringBuffer();
        sb.append("hello");
        sb.append("world");
        // JIT編譯器通過逃逸分析發現sb僅在方法內部使用,不存在競爭,可以消除鎖
    }
}

鎖消除優化默認啓用,可以通過參數-XX:+DoEscapeAnalysis -XX:+EliminateLocks控制(逃逸分析是鎖消除的前提)。只有在確認對象不會"逃逸"到當前線程之外被其他線程訪問時,JVM 才會消除鎖。

2. 鎖粗化

JVM 檢測到連續對同一鎖的請求與釋放操作,會將多個連續的鎖操作合併為一個更大範圍的鎖。這減少了加鎖解鎖的頻繁操作,提高性能。

鎖粗化中的"連續"指的是無其他代碼插入的緊接同步塊

public class LockCoarseningDemo {
    public void method() {
        // 鎖粗化可能生效(中間無其他操作)
        synchronized(this) { System.out.println("操作1"); }
        synchronized(this) { System.out.println("操作2"); }
        synchronized(this) { System.out.println("操作3"); }

        // JVM優化後可能變為:
        /*
        synchronized(this) {
            System.out.println("操作1");
            System.out.println("操作2");
            System.out.println("操作3");
        }
        */

        // 而下面的代碼,鎖粗化可能不生效(中間有其他邏輯)
        synchronized(this) { System.out.println("操作A"); }
        System.out.println("非同步代碼");
        synchronized(this) { System.out.println("操作B"); }
    }
}

3. 自適應自旋

輕量級鎖自旋等待時,JVM 會根據上一次自旋等待的成功與否以及鎖的持有時間動態調整自旋的次數。如果前一次自旋成功獲得過鎖,那麼下一次自旋的次數可能會更多;如果自旋很少成功獲得鎖,那麼會減少自旋次數或者直接升級為重量級鎖。

自旋通過忙等待(Busy Waiting)消耗 CPU 資源,適用於臨界區執行時間非常短的場景。在這種情況下,線程阻塞/喚醒的開銷可能遠大於自旋等待的開銷。但如果臨界區執行時間較長,持有鎖的線程不會很快釋放鎖,此時自旋會白白消耗 CPU 資源,反而導致性能下降,這種情況下升級為重量級鎖更為合適。

自適應自旋默認啓用,可通過-XX:-UseSpinning禁用(在新版 JDK 中被移除,默認總是啓用自旋)。

五、類鎖與對象鎖的區別

類鎖與對象鎖的本質區別在於鎖定的對象不同:

  • 類鎖:鎖的是類的 Class 對象,全局唯一
  • 對象鎖:鎖的是實例對象,每個實例都有獨立的鎖
public class LockTypeDemo {
    // 對象鎖:修飾實例方法
    public synchronized void instanceMethod() {
        System.out.println(Thread.currentThread().getName() + " 獲取對象鎖");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 類鎖:修飾靜態方法
    public static synchronized void staticMethod() {
        System.out.println(Thread.currentThread().getName() + " 獲取類鎖");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        final LockTypeDemo instance1 = new LockTypeDemo();
        final LockTypeDemo instance2 = new LockTypeDemo();

        // 測試對象鎖
        new Thread(() -> instance1.instanceMethod(), "Thread-1").start();
        new Thread(() -> instance2.instanceMethod(), "Thread-2").start();

        // 測試類鎖
        new Thread(() -> LockTypeDemo.staticMethod(), "Thread-3").start();
        new Thread(() -> LockTypeDemo.staticMethod(), "Thread-4").start();

        // 測試對象鎖與類鎖的互不干擾
        new Thread(() -> {
            instance1.instanceMethod(); // 獲取對象鎖
        }, "Thread-5").start();

        new Thread(() -> {
            LockTypeDemo.staticMethod(); // 獲取類鎖
        }, "Thread-6").start();
    }
}

運行結果表明:

  • 不同對象的對象鎖互不干擾(Thread-1 和 Thread-2 可同時執行)
  • 所有類鎖是同一把鎖(Thread-3 和 Thread-4 互斥)
  • 對象鎖和類鎖互不干擾(Thread-5 和 Thread-6 可同時執行,因為它們鎖定的是不同的對象)

類鎖和對象鎖本質上都是對象鎖,只是鎖的對象不同:類鎖鎖的是 Class 對象,而對象鎖鎖的是實例對象。

六、實戰案例分析

案例 1:死鎖問題診斷與修復

public class DeadLockDemo {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (LOCK_A) {
                System.out.println("Thread 1: Holding lock A...");
                try { Thread.sleep(1000); } catch (Exception e) {}
                System.out.println("Thread 1: Waiting for lock B...");

                synchronized (LOCK_B) {
                    System.out.println("Thread 1: Holding lock A & B");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (LOCK_B) {
                System.out.println("Thread 2: Holding lock B...");
                try { Thread.sleep(1000); } catch (Exception e) {}
                System.out.println("Thread 2: Waiting for lock A...");

                synchronized (LOCK_A) {
                    System.out.println("Thread 2: Holding lock A & B");
                }
            }
        }).start();
    }
}

問題分析:兩個線程分別持有一把鎖,同時等待對方釋放另一把鎖,形成死鎖。

解決方案

  1. 統一鎖獲取順序:所有線程按照相同的順序獲取鎖(先獲取 LOCK_A,再獲取 LOCK_B)
  2. 使用超時機制:使用 ReentrantLock 的 tryLock(timeout)方法,避免無限期等待
  3. 避免嵌套鎖:重構代碼,避免在持有一把鎖的情況下再獲取另一把鎖
// 解決方案1:統一鎖獲取順序
public class DeadLockSolution1 {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            // 兩個線程都先獲取LOCK_A,再獲取LOCK_B
            synchronized (LOCK_A) {
                System.out.println("Thread 1: Holding lock A...");
                try { Thread.sleep(1000); } catch (Exception e) {}

                synchronized (LOCK_B) {
                    System.out.println("Thread 1: Holding lock A & B");
                }
            }
        }).start();

        new Thread(() -> {
            // 統一按照相同順序獲取鎖
            synchronized (LOCK_A) {
                System.out.println("Thread 2: Holding lock A...");
                try { Thread.sleep(1000); } catch (Exception e) {}

                synchronized (LOCK_B) {
                    System.out.println("Thread 2: Holding lock A & B");
                }
            }
        }).start();
    }
}

案例 2:性能對比測試

下面通過測試比較不同鎖類型的性能差異:

public class LockPerformanceTest {
    private static final int THREAD_COUNT = 10;
    private static final int LOOP_COUNT = 100000;

    // 測試不同鎖的性能
    public static void main(String[] args) throws Exception {
        // 1. 無鎖
        testNoLock();

        // 2. 對象鎖(synchronized方法)
        testSynchronizedMethod();

        // 3. 對象鎖(synchronized塊)
        testSynchronizedBlock();

        // 4. 類鎖(static synchronized方法)
        testStaticSynchronizedMethod();

        // 5. 重入鎖(ReentrantLock)
        testReentrantLock();
    }

    // 無鎖實現(非線程安全)
    private static void testNoLock() throws Exception {
        long startTime = System.currentTimeMillis();
        final Counter counter = new Counter();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP_COUNT; j++) {
                    counter.noLockIncrement();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("無鎖操作: " + (endTime - startTime) + "ms, 結果: " + counter.noLockCount);
    }

    // 對象鎖(synchronized方法)測試
    private static void testSynchronizedMethod() throws Exception {
        long startTime = System.currentTimeMillis();
        final Counter counter = new Counter();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP_COUNT; j++) {
                    counter.syncMethodIncrement();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("synchronized方法: " + (endTime - startTime) + "ms, 結果: " + counter.syncMethodCount);
    }

    // 對象鎖(synchronized塊)測試
    private static void testSynchronizedBlock() throws Exception {
        long startTime = System.currentTimeMillis();
        final Counter counter = new Counter();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP_COUNT; j++) {
                    counter.syncBlockIncrement();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("synchronized塊: " + (endTime - startTime) + "ms, 結果: " + counter.syncBlockCount);
    }

    // 類鎖(static synchronized方法)測試
    private static void testStaticSynchronizedMethod() throws Exception {
        long startTime = System.currentTimeMillis();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP_COUNT; j++) {
                    Counter.staticSyncMethodIncrement();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("靜態synchronized方法: " + (endTime - startTime) + "ms, 結果: " + Counter.staticSyncMethodCount);
    }

    // ReentrantLock測試
    private static void testReentrantLock() throws Exception {
        long startTime = System.currentTimeMillis();
        final Counter counter = new Counter();

        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < LOOP_COUNT; j++) {
                    counter.reentrantLockIncrement();
                }
            });
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("ReentrantLock: " + (endTime - startTime) + "ms, 結果: " + counter.reentrantLockCount);
    }

    // 計數器類
    static class Counter {
        private int noLockCount = 0;
        private int syncMethodCount = 0;
        private int syncBlockCount = 0;
        private static int staticSyncMethodCount = 0;
        private final Lock lock = new ReentrantLock();
        private int reentrantLockCount = 0;

        // 無鎖增加
        public void noLockIncrement() {
            noLockCount++;
        }

        // synchronized方法
        public synchronized void syncMethodIncrement() {
            syncMethodCount++;
        }

        // synchronized塊
        public void syncBlockIncrement() {
            synchronized(this) {
                syncBlockCount++;
            }
        }

        // 靜態synchronized方法
        public static synchronized void staticSyncMethodIncrement() {
            staticSyncMethodCount++;
        }

        // ReentrantLock
        public void reentrantLockIncrement() {
            lock.lock();
            try {
                reentrantLockCount++;
            } finally {
                lock.unlock();
            }
        }
    }
}

典型測試結果(i7 處理器,8GB 內存,JDK 1.8):

  • 無鎖操作:約 15ms(結果不正確,存在線程安全問題)
  • synchronized 方法:約 85ms
  • synchronized 塊:約 80ms(略優於方法同步)
  • 靜態 synchronized 方法:約 90ms(類鎖競爭相同)
  • ReentrantLock:約 75ms

JDK 1.6 後,synchronized 性能得到極大提升,已經接近顯式鎖 ReentrantLock 的性能。在單線程情況下,偏向鎖的性能接近無鎖操作;在低競爭情況下,輕量級鎖通過自旋避免了線程阻塞/喚醒的開銷。

七、synchronized 使用注意事項

  1. 減小鎖粒度:鎖定需要同步的代碼塊,而非整個方法
// 改進前
public synchronized void processData(List<String> data) {
    // 準備階段(無需同步)
    String threadName = Thread.currentThread().getName();
    System.out.println(threadName + "準備處理數據...");

    // 處理數據(需要同步)
    for (String item : data) {
        // 臨界區操作
    }

    // 收尾工作(無需同步)
    System.out.println(threadName + "處理完成");
}

// 改進後
public void processData(List<String> data) {
    // 準備階段(無需同步)
    String threadName = Thread.currentThread().getName();
    System.out.println(threadName + "準備處理數據...");

    // 只同步需要的代碼塊
    synchronized(this) {
        for (String item : data) {
            // 臨界區操作
        }
    }

    // 收尾工作(無需同步)
    System.out.println(threadName + "處理完成");
}
  1. 避免鎖對象被修改:使用 final 修飾鎖對象
// 錯誤示例
class UnsafeLock {
    private Object lock = new Object();

    public void method() {
        synchronized(lock) {
            // 臨界區代碼
        }
    }

    public void changeLock() {
        lock = new Object(); // 危險!更換鎖對象
    }
}

// 正確示例
class SafeLock {
    private final Object lock = new Object();

    public void method() {
        synchronized(lock) {
            // 臨界區代碼
        }
    }
}
  1. 避免"死等":synchronized 不可中斷,考慮使用可中斷的 ReentrantLock
  2. 注意可重入性:synchronized 是可重入鎖,同一線程可以多次獲取自己持有的鎖
public class ReentrantDemo {
    public synchronized void outer() {
        System.out.println("進入外層方法");
        inner(); // 可重入,不會導致死鎖
    }

    public synchronized void inner() {
        System.out.println("進入內層方法");
    }
}

八、總結

特性 偏向鎖 輕量級鎖 重量級鎖
使用場景 單線程訪問 多線程交替訪問 多線程競爭訪問
競爭機制 無競爭 CAS 自旋 互斥量+線程阻塞
性能開銷 最低 較低 最高
優點 單線程性能最佳 無需線程阻塞喚醒 穩定性好
缺點 只適合單線程 自旋消耗 CPU 線程阻塞+上下文切換
標誌位 01+1(偏向標記),存儲線程 ID+epoch 00,Mark Word 存儲指向棧中鎖記錄的指針 10,Mark Word 存儲指向 Monitor 對象的指針

synchronized 作為 Java 多線程編程中最基礎的同步機制,經過 JDK 不斷優化已經具備很好的性能。理解其底層原理和鎖升級過程,有助於我們更高效地使用它,也為解決實際併發問題提供思路。在實際開發中,應結合具體場景選擇合適的同步策略,減小鎖粒度,避免競爭,從而構建高性能的併發應用。

在下一篇文章中,我們將探討 ReentrantLock 高級特性與應用,敬請期待!


感謝您耐心閲讀到這裏!如果覺得本文對您有幫助,歡迎點贊 👍、收藏 ⭐、分享給需要的朋友,您的支持是我持續輸出技術乾貨的最大動力!

如果想獲取更多 Java 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~

user avatar u_10983731 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.