JUC學習筆記02

文章目錄

  • JUC筆記2
    • 練習題:手寫線程池
      • 代碼解釋:
        • AdvancedThreadPool 類:
        • WorkerThread 內部類:
        • AdvancedThreadPoolExample 類:
    • 線程池的思考
      • CPU密集型
      • IO密集型
    • 練習題:手寫自動重試機
    • 練習題:手寫定時任務機
      • Job封裝
      • 定時任務執行
    • 附錄1:常見函數式接口
    • 附錄2:ForkJoin
      • 1. ForkJoin 框架概述
      • 2. 核心組件
        • 2.1 ForkJoinPool
        • 2.2 ForkJoinTask
      • 3. 使用案例
        • 3.1 計算數組元素之和(使用 `RecursiveTask`)
        • 3.2 打印數組元素(使用 `RecursiveAction`)
      • 4. 總結
    • 附錄3:異步回調
    • 重點:JMM,VOLATILE,單例,CAS,原子引用,各種鎖的深入理解
      • JMM(Java Memory Model)
        • 實際的執行模型參考
        • 現在考慮更復雜一點的狀況,B線程對Flag做了修改
      • VOLATILE
      • 單例
        • 1. 餓漢式(靜態常量)
        • 2. 餓漢式(靜態代碼塊)
        • 3. 懶漢式(線程不安全)
        • 4. 懶漢式(同步方法,線程安全)
        • 5. 雙重檢查鎖定(Double-Checked Locking,線程安全)
        • 6. 靜態內部類(線程安全)
        • 7. 枚舉(線程安全)
        • 實現原理
        • 代碼示例
        • 調用示例
        • 代碼解釋
        • 優勢
        • 總結
        • 附錄:雙重檢查鎖定的詳細解釋
      • CAS
        • 基本概念
        • 原理
        • 在 Java 中的實現
        • CAS 在 JUC 中的應用
        • 優缺點
          • 優點
          • 缺點
        • 附錄:unsafe的底層C++
      • 原子引用
      • 常見鎖的提法
        • 公平與非公平鎖
        • 樂觀鎖與悲觀鎖
        • 可重入鎖
        • 偏向鎖
        • 自旋鎖
        • 表鎖行鎖間隙鎖及MVCC
        • 死鎖及排查

JUC筆記2

練習題:手寫線程池

注意,要實現的目標如下:

  1. 核心線程不會在空閑時自動死亡,而輔助線程會在空閑一段時間后自動消亡
  2. 任務添加失敗時會自旋重新嘗試添加任務若干次,等待時間和重試次數可自定義
  3. 線程池可以手動強停
  4. 線程池的核心線程數可以動態調整(縮減時需要格外注意)
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;// 自定義線程池類
class AdvancedThreadPool {private int corePoolSize;private int maximumPoolSize;private final BlockingQueue<Runnable> taskQueue;private final List<WorkerThread> workerThreads;private boolean isShutdown = false;private int retryCount;private long retryDelay;private long keepAliveTime;// 構造函數,初始化線程池public AdvancedThreadPool(int corePoolSize, int maximumPoolSize, int queueCapacity, int retryCount, long retryDelay, long keepAliveTime) {this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.taskQueue = new LinkedBlockingQueue<>(queueCapacity);this.workerThreads = new ArrayList<>(maximumPoolSize);this.retryCount = retryCount;this.retryDelay = retryDelay;this.keepAliveTime = keepAliveTime;// 初始化核心線程for (int i = 0; i < corePoolSize; i++) {WorkerThread worker = new WorkerThread(true);workerThreads.add(worker);worker.start();}}// 動態設置核心線程數public void setCorePoolSize(int corePoolSize) {if (corePoolSize < 0) {throw new IllegalArgumentException("核心線程數不能為負數");}this.corePoolSize = corePoolSize;adjustThreadCount();}// 動態設置最大線程數public void setMaximumPoolSize(int maximumPoolSize) {if (maximumPoolSize < 0) {throw new IllegalArgumentException("最大線程數不能為負數");}this.maximumPoolSize = maximumPoolSize;adjustThreadCount();}// 調整線程數量private void adjustThreadCount() {int currentSize = workerThreads.size();if (corePoolSize > currentSize) {// 需要增加線程for (int i = currentSize; i < corePoolSize; i++) {WorkerThread worker = new WorkerThread(true);workerThreads.add(worker);worker.start();}} else if (corePoolSize < currentSize) {// 需要減少線程(高危操作,實際生產中未必需要這個部分)Iterator<WorkerThread> iterator = workerThreads.iterator();int removedCount = 0;while (iterator.hasNext() && removedCount < currentSize - corePoolSize) {WorkerThread worker = iterator.next();if (!worker.isCore) {worker.interrupt();iterator.remove();removedCount++;}}}}// 向線程池提交任務public void submit(Runnable task) {if (isShutdown) {throw new IllegalStateException("線程池已關閉,不能再提交任務。");}int attempts = 0;while (attempts < retryCount) {try {if (taskQueue.offer(task, retryDelay, TimeUnit.MILLISECONDS)) {break;}} catch (InterruptedException e) {Thread.currentThread().interrupt();}attempts++;}if (attempts == retryCount) {if (workerThreads.size() < maximumPoolSize) {// 創建新的輔助線程來處理任務WorkerThread worker = new WorkerThread(false);workerThreads.add(worker);worker.start();try {taskQueue.put(task);} catch (InterruptedException e) {Thread.currentThread().interrupt();}} else {throw new RuntimeException("任務隊列已滿,且達到最大線程數,無法處理任務。");}}}// 關閉線程池public void shutdown() {isShutdown = true;for (WorkerThread worker : workerThreads) {worker.interrupt();}}// 工作線程類,負責從任務隊列中取出任務并執行private class WorkerThread extends Thread {private final boolean isCore;public WorkerThread(boolean isCore) {this.isCore = isCore;}@Overridepublic void run() {while (!isShutdown ||!taskQueue.isEmpty()) {Runnable task = null;try {if (isCore) {// 核心線程使用 take 方法,會一直阻塞直到有任務task = taskQueue.take();} else {// 輔助線程使用 poll 方法,在空閑時間后沒有任務則退出task = taskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS);if (task == null) {// 輔助線程空閑超時,從線程列表中移除自身synchronized (workerThreads) {workerThreads.remove(this);}break;}}} catch (InterruptedException e) {// 如果線程被中斷,跳出循環break;}if (task != null) {try {task.run();} catch (Exception e) {// 處理任務執行時的異常e.printStackTrace();}}}}}
}// 測試線程池的使用
public class AdvancedThreadPoolExample {public static void main(String[] args) {// 創建一個線程池,核心線程數為 2,最大線程數為 5,任務隊列容量為 3,重試次數為 3,重試延遲為 500 毫秒,輔助線程空閑存活時間為 2 秒AdvancedThreadPool threadPool = new AdvancedThreadPool(2, 5, 3, 3, 500, 2000);// 提交 10 個任務到線程池for (int i = 0; i < 10; i++) {final int taskId = i;threadPool.submit(() -> {System.out.println("正在執行任務: " + taskId + ",線程: " + Thread.currentThread().getName());try {// 模擬任務執行時間Thread.sleep(1000);} catch (InterruptedException e) {Thread.currentThread().interrupt();}});}// 動態調整核心線程數threadPool.setCorePoolSize(3);try {// 等待一段時間,讓輔助線程有機會空閑消亡Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}// 關閉線程池threadPool.shutdown();}
}

代碼解釋:

AdvancedThreadPool 類:
  1. 新增成員變量

    • keepAliveTime:輔助線程的空閑存活時間,當輔助線程空閑時間超過該值時,會自動退出。
  2. 構造函數

    • 增加了 keepAliveTime 參數的初始化,并創建核心線程。
  3. adjustThreadCount() 方法

    • 在調整線程數量時,優先移除輔助線程,以保證核心線程不被誤移除。
  4. submit(Runnable task) 方法

    • 與之前的實現相同,處理任務提交和重試邏輯。
  5. shutdown() 方法

    • 標記線程池為關閉狀態,并中斷所有工作線程。
WorkerThread 內部類:
  1. isCore 字段

    • 用于標記該線程是核心線程還是輔助線程。
  2. run() 方法

    • 核心線程使用 taskQueue.take() 方法,該方法會一直阻塞直到有任務到來,因此核心線程不會因空閑而死亡。
    • 輔助線程使用 taskQueue.poll(keepAliveTime, TimeUnit.MILLISECONDS) 方法,在空閑 keepAliveTime 時間后,如果沒有任務則退出,并從 workerThreads 列表中移除自身。
AdvancedThreadPoolExample 類:
  • 創建線程池時傳入 keepAliveTime 參數。
  • 提交任務并動態調整核心線程數。
  • 主線程休眠一段時間,讓輔助線程有機會空閑消亡。
  • 最后關閉線程池。

線程池的思考

究竟該怎么決定核心線程數是多少? 此處特別感謝B站UP@狂神說Java 在JUC相關視頻中關于核心線程數的一些考量

CPU密集型

直接把CPU的核數作為最大線程數:
new ThreadPoolExecutor(
2,
Runtime.getRuntime().availableProcessors(), // 根據最大核心數決定
3,
TimeUnit.SECONDS,
// 阻塞隊列,
// ThreadFactory
// 淘汰策略
)

IO密集型

很好理解,你的項目里有若干個,比如K個需要IO的大任務,這些任務的總數拿來設定最大核心數,起碼保證這些任務不崩

練習題:手寫自動重試機

目標:

  1. 允許泛型出入參
  2. 允許自定義循環重試的條件
  3. 允許重試開始前自定義回調函數

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;/*** 用于自動重試某個函數,T為入參,R為返回值** @param <T> action入參* @param <R> 返回值*/
public class MineActionUpgrade<T, R> {private static final Logger LOGGER = Logger.getLogger(MineActionUpgrade.class.getName());/*** 待執行函數*/private final Function<T, R> action;/*** 重試次數*/private final int retryLimitTimes;/*** 重試間隔*/private final long retryInterval;/*** 重試判斷函數*/private final Function<Optional<R>, Boolean> retryCondition;/*** 重試前回調函數*/private final Function<Integer, Void> beforeRetryCallback;public MineActionUpgrade(Function<T, R> action, int retryLimitTimes, long retryInterval) {this(action, retryLimitTimes, retryInterval, Optional::isEmpty, null);}public MineActionUpgrade(Function<T, R> action, int retryLimitTimes, long retryInterval,Function<Optional<R>, Boolean> retryCondition,Function<Integer, Void> beforeRetryCallback) {if (action == null) {throw new IllegalArgumentException("待執行體為null");}if (retryLimitTimes <= 0) {throw new IllegalArgumentException("可重試次數不合法");}if (retryInterval <= 0) {throw new IllegalArgumentException("重試間隔不合法");}this.action = action;this.retryLimitTimes = retryLimitTimes;this.retryInterval = retryInterval;this.retryCondition = Objects.requireNonNullElse(retryCondition, Optional::isEmpty);this.beforeRetryCallback = beforeRetryCallback;}/*** 自動執行** @param param 執行參數* @return 返回值*/public Optional<R> exec(T param) {for (int i = 0; i < retryLimitTimes; i++) {if (i > 0 && beforeRetryCallback != null) {beforeRetryCallback.apply(i);}Optional<R> result = execOnce(param);if (!retryCondition.apply(result)) {return result;}try {TimeUnit.MILLISECONDS.sleep(retryInterval);} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("執行過程中線程被中斷", e);}}throw new RuntimeException("函數重試次數耗盡仍未獲得結果");}private Optional<R> execOnce(T param) {R res = null;try {R apply = action.apply(param);if (Objects.nonNull(apply)) {res = apply;}} catch (Exception e) {LOGGER.log(Level.SEVERE, "執行失敗,參數為:" + param, e);}return Optional.ofNullable(res);}
}

練習題:手寫定時任務機

特別地:此處感謝B站UP提供的思路@學習Java的基爾獸

目標:

  1. 支持自定義時間定時執行某個函數部分

Job封裝

import java.util.Objects;/*** 任務封裝*/
public class Job implements Comparable<Job> {/*** 待執行任務*/private Runnable task;/*** 下次開始時間*/private long startTime;/*** 需要等待時間*/private long delay;public Runnable getTask() {return task;}public long getStartTime() {return startTime;}public long getDelay() {return delay;}private Job() {}public Job(Runnable task, long startTime, long delay) {if (Objects.isNull(task)) {throw new IllegalArgumentException("待執行任務不能為Null");}if (startTime <= 0) {throw new IllegalArgumentException("開始時間非法");}if (delay <= 0) {throw new IllegalArgumentException("等待時間非法");}this.task = task;this.startTime = startTime;this.delay = delay;}/*** 用于排序任務** @param o the object to be compared.* @return 排序結果*/@Overridepublic int compareTo(Job o) {return Long.compare(this.startTime, o.startTime);}
}

定時任務執行


public class MineSchedule {private final ExecutorService service = Executors.newFixedThreadPool(6);private final Trigger trigger = new Trigger();class Trigger {/*** 優先級隊列,會自動排序*/PriorityBlockingQueue<Job> queue = new PriorityBlockingQueue<>();Thread machine = new Thread(() -> {while (true) {while (queue.isEmpty()) {LockSupport.park();}Job latelyJob = queue.peek();if (latelyJob.getStartTime() < System.currentTimeMillis()) {latelyJob = queue.poll();if (latelyJob != null) {service.execute(latelyJob.getTask());queue.offer(rebuildJob(latelyJob));}} else {LockSupport.parkUntil(latelyJob.getStartTime());}}});{machine.start();System.out.println("觸發器啟動");}void wakeUp() {LockSupport.unpark(machine);}private Job rebuildJob(Job old) {return new Job(old.getTask(), old.getStartTime() + old.getDelay(), old.getDelay());}}/*** 每隔delay毫秒數,自動執行一次task** @param task  需要周期執行的任務* @param delay 延遲時間*/public void schedule(Runnable task, long delay) {// 最開始的想法,搞一個線程池,每次有新任務的時候把任務丟進去,睡delay毫秒后執行// 但是這是有問題的,線程耗盡就完了,而且線程不可復用,創建線程消耗資源很大// 那我們就考慮這么一種設計:// 1. 有一個定時觸發器,每隔delay時間被喚醒,然后去嘗試執行任務// 2. 線程池只負責執行任務,不負責處理時間// 那么這個觸發器需要什么信息呢?第一,所有需要執行的任務,第二,需要delay的時間// 那么我們封裝一個Job類,專門用來記錄任務和時間// 再寫一個trigger,用于時間觸發Job job = new Job(task, System.currentTimeMillis(), delay);trigger.queue.offer(job);trigger.wakeUp();}
}

附錄1:常見函數式接口

Function: 自定義一個帶有出入參的接口
Consumer:自定義一個消費接口,只有入參
Supplier:自定義一個生產接口,只有出參
Predicate:自定義一個判斷接口,只返回布爾值

附錄2:ForkJoin

1. ForkJoin 框架概述

ForkJoin 框架是 Java 7 引入的一個并行執行框架,它主要用于處理可以遞歸分解成更小任務的計算密集型任務。其核心思想是將一個大任務拆分成多個小任務(fork 操作),并行地執行這些小任務,然后將小任務的結果合并(join 操作)得到最終結果。ForkJoin 框架利用多線程并行計算,充分發揮多核處理器的性能,提高程序的執行效率。

2. 核心組件

2.1 ForkJoinPool

ForkJoinPoolForkJoin 框架的核心,它是一個特殊的線程池,用于管理和執行 ForkJoinTask 任務。ForkJoinPool 采用工作竊取算法(Work-Stealing Algorithm),每個工作線程都有自己的雙端隊列(Deque),當一個線程完成了自己隊列中的任務后,它可以從其他線程的隊列尾部竊取任務來執行,從而充分利用線程資源,提高并行度。

2.2 ForkJoinTask

ForkJoinTask 是所有任務的抽象基類,它有兩個重要的子類:

  • RecursiveAction:用于沒有返回值的任務。
  • RecursiveTask:用于有返回值的任務。

3. 使用案例

3.1 計算數組元素之和(使用 RecursiveTask

下面是一個使用 ForkJoin 框架計算數組元素之和的示例代碼:

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;// 繼承 RecursiveTask 用于有返回值的任務
class SumTask extends RecursiveTask<Integer> {private static final int THRESHOLD = 10;private int[] array;private int start;private int end;public SumTask(int[] array, int start, int end) {this.array = array;this.start = start;this.end = end;}@Overrideprotected Integer compute() {if (end - start <= THRESHOLD) {// 任務足夠小,直接計算int sum = 0;for (int i = start; i < end; i++) {sum += array[i];}return sum;} else {// 任務過大,拆分任務int mid = (start + end) / 2;SumTask leftTask = new SumTask(array, start, mid);SumTask rightTask = new SumTask(array, mid, end);// 執行左子任務leftTask.fork();// 執行右子任務并獲取結果int rightResult = rightTask.compute();// 獲取左子任務的結果int leftResult = leftTask.join();// 合并結果return leftResult + rightResult;}}
}public class ForkJoinSumExample {public static void main(String[] args) {int[] array = new int[100];for (int i = 0; i < array.length; i++) {array[i] = i + 1;}// 創建 ForkJoinPoolForkJoinPool forkJoinPool = new ForkJoinPool();// 創建任務SumTask sumTask = new SumTask(array, 0, array.length);// 執行任務并獲取結果int result = forkJoinPool.invoke(sumTask);System.out.println("數組元素之和為: " + result);}
}

代碼解釋

  • SumTask:繼承自 RecursiveTask<Integer>,表示這是一個有返回值的任務,返回值類型為 Integer
  • compute 方法:該方法是任務的核心邏輯,首先判斷任務是否足夠小(即元素數量是否小于等于閾值 THRESHOLD),如果足夠小則直接計算元素之和;否則,將任務拆分成兩個子任務(左子任務和右子任務),分別調用 fork() 方法異步執行左子任務,調用 compute() 方法同步執行右子任務,最后調用 join() 方法獲取左子任務的結果,并將兩個結果合并。
  • ForkJoinPool:創建一個 ForkJoinPool 實例,調用 invoke() 方法執行任務并獲取最終結果。
3.2 打印數組元素(使用 RecursiveAction

下面是一個使用 RecursiveAction 打印數組元素的示例代碼:

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;// 繼承 RecursiveAction 用于無返回值的任務
class PrintTask extends RecursiveAction {private static final int THRESHOLD = 10;private int[] array;private int start;private int end;public PrintTask(int[] array, int start, int end) {this.array = array;this.start = start;this.end = end;}@Overrideprotected void compute() {if (end - start <= THRESHOLD) {// 任務足夠小,直接打印元素for (int i = start; i < end; i++) {System.out.print(array[i] + " ");}} else {// 任務過大,拆分任務int mid = (start + end) / 2;PrintTask leftTask = new PrintTask(array, start, mid);PrintTask rightTask = new PrintTask(array, mid, end);// 執行左子任務leftTask.fork();// 執行右子任務rightTask.compute();// 等待左子任務完成leftTask.join();}}
}public class ForkJoinPrintExample {public static void main(String[] args) {int[] array = new int[100];for (int i = 0; i < array.length; i++) {array[i] = i + 1;}// 創建 ForkJoinPoolForkJoinPool forkJoinPool = new ForkJoinPool();// 創建任務PrintTask printTask = new PrintTask(array, 0, array.length);// 執行任務forkJoinPool.invoke(printTask);}
}

代碼解釋

  • PrintTask:繼承自 RecursiveAction,表示這是一個沒有返回值的任務。
  • compute 方法:與 SumTask 類似,首先判斷任務是否足夠小,如果足夠小則直接打印元素;否則,將任務拆分成兩個子任務,分別調用 fork() 方法異步執行左子任務,調用 compute() 方法同步執行右子任務,最后調用 join() 方法等待左子任務完成。
  • ForkJoinPool:創建一個 ForkJoinPool 實例,調用 invoke() 方法執行任務。

4. 總結

ForkJoin 框架適用于處理可以遞歸分解的計算密集型任務,通過并行執行小任務和合并結果,可以充分利用多核處理器的性能。在使用時,需要根據任務是否有返回值選擇繼承 RecursiveTaskRecursiveAction,并在 compute 方法中實現任務的拆分和合并邏輯。

附錄3:異步回調

其實就是Future,我們在Callable中已經使用過
這里要說的是實際開發中大家經常用CompletableFuture,用起來很簡單很爽,但是這個東西是有隱患的

import java.util.concurrent.CompletableFuture;public class test {public static void main(String[] args) {// 常見但有問題的用法CompletableFuture.runAsync(() -> {// 代碼});}
}

這里的問題在于CompletableFuture只給一個Runnable作為參數那么它會直接使用JVM的公共線程池,這對項目的壓力是很大的,并發拉上來以后,壓力會均勻傳導到所有功能上拖慢服務。
正確的用法:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class test {public static void main(String[] args) {// 自定義線程池。注意,這里是為了簡便所以用newFixedThreadPool,實際工程中應當根據情況使用ThreadPoolExecutor來手動創建可靠線程池,避免壓力傳導ExecutorService service = Executors.newFixedThreadPool(6);CompletableFuture.runAsync(() -> {// 代碼}, service);}
}

重點:JMM,VOLATILE,單例,CAS,原子引用,各種鎖的深入理解

JMM(Java Memory Model)

是一個虛擬概念,并不實際存在,用于模擬JVM的內存模型,是一種約定而非實在。相關資料可以參考:
JMM參考文檔1
JMM參考文檔2

JMM可以做到什么?

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排

指令重排:開發者所寫的程序,會被編譯器編譯到字節碼
過程為:源碼->編譯器優化重排->指令并行重排->內存系統重排->執行。
使用volatile可以禁止代碼重新排序。這個部分一般不會問到,牽涉到C++的一些功能,和java的關系有但是不那么大

約定的內容:

  1. 當線程解鎖前,必須把共享變量立刻刷回:a.變量存儲在主存中;b.線程需要使用變量;c.線程把主存中的變量拷貝到線程的工作空間中;d.線程修改變量;e.線程即將釋放,馬上把修改后的變量刷回
    線程獲取變量的數據模型

  2. 當線程加鎖前,必須讀取主存的最新值到工作空間

  3. 加解鎖必須是同一把鎖

實際的執行模型參考

在這里插入圖片描述

其中,read和load是一組,Use和Assign是一組,write和store是一組,同組操作不可分割。
除了這三組操作以外,我們還需要注意Lock和UnLock,這兩個操作沒有出現在圖中,但多線程時必定使用,下面說。

現在考慮更復雜一點的狀況,B線程對Flag做了修改

在這里插入圖片描述

B線程把Flag修改為false,但A這里仍然在使用True,這就會出現問題。

/*** 實際案例看沒有volatile,變量會出什么問題*/
public class TestJmm01 {private static int num = 0;public static void main(String[] args) throws InterruptedException {new Thread(() -> {while (num == 0) {}}).start();TimeUnit.SECONDS.sleep(1);// 由于num對上面的Thread不可見,所以雖然我們把num修改為1,但線程仍然無限死循環num = 1;System.out.println(num);}}

這時候VOLATILE就有用了

public class TestJmm02 {private volatile static int num = 0;public static void main(String[] args) throws InterruptedException {new Thread(() -> {while (num == 0) {}}).start();TimeUnit.SECONDS.sleep(1);// 由于num對上面的Thread不可見,所以雖然我們把num修改為1,但線程仍然無限死循環num = 1;System.out.println(num);}
}

VOLATILE

輕量級的同步機制,不能保證原子性

如上所述,Volatile標記的變量只能保證自己對所有線程都可見,但不能確保在一組操作中,可以讓一組操作都成功或失敗
可以從下面這個案例看到:
在這里插入圖片描述

圖中add函數雖然只是num++一行代碼,但是在字節碼中我們很容易發現,這是一個復合操作,分為讀,改,寫回幾步,這個過程中并沒有事務特性,不保證原子性。
那這里如果我們就是不想用synchronized或者lock,應該怎么辦呢?
用atomicInteger,原子整型。
為什么原子類可以做到?因為底層是UnSafe,用native方法通過CAS實現的。這個部分在下面說。

單例

在 Java 中,單例模式是一種常見的設計模式,它確保一個類只有一個實例,并提供一個全局訪問點來獲取這個實例。為了保證在多線程環境下也能正確地創建和使用單例實例,需要實現線程安全的單例模式。以下是幾種常見的實現方式:

1. 餓漢式(靜態常量)
public class Singleton {// 靜態常量,在類加載時就創建實例private static final Singleton INSTANCE = new Singleton();// 私有構造函數,防止外部實例化private Singleton() {}// 提供公共的靜態方法獲取實例public static Singleton getInstance() {return INSTANCE;}
}

解釋

  • 這種方式在類加載時就創建了單例實例,由于類加載過程是線程安全的,所以這種實現方式天然就是線程安全的。
  • 缺點是如果這個單例實例在程序運行過程中可能不會被使用,那么它會一直占用內存,造成資源浪費。
2. 餓漢式(靜態代碼塊)
public class Singleton {private static Singleton INSTANCE;static {// 在靜態代碼塊中創建實例INSTANCE = new Singleton();}private Singleton() {}public static Singleton getInstance() {return INSTANCE;}
}

解釋

  • 同樣是在類加載時創建實例,利用靜態代碼塊的特性保證線程安全。
  • 缺點和靜態常量方式一樣,可能會造成資源浪費。
3. 懶漢式(線程不安全)
public class Singleton {private static Singleton INSTANCE;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
}

解釋

  • 這種方式在第一次調用 getInstance() 方法時才創建實例,實現了懶加載。
  • 但在多線程環境下,可能會有多個線程同時進入 if (INSTANCE == null) 語句塊,從而創建多個實例,因此是線程不安全的。
4. 懶漢式(同步方法,線程安全)
public class Singleton {private static Singleton INSTANCE;private Singleton() {}// 使用 synchronized 關鍵字保證線程安全public static synchronized Singleton getInstance() {if (INSTANCE == null) {INSTANCE = new Singleton();}return INSTANCE;}
}

解釋

  • 通過在 getInstance() 方法上添加 synchronized 關鍵字,保證了同一時間只有一個線程可以進入該方法,從而避免了多線程創建多個實例的問題。
  • 缺點是每次調用 getInstance() 方法都需要進行同步,會帶來一定的性能開銷。
5. 雙重檢查鎖定(Double-Checked Locking,線程安全)
public class Singleton {// 使用 volatile 關鍵字保證可見性和禁止指令重排序private static volatile Singleton INSTANCE;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {synchronized (Singleton.class) {if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}
}

解釋

  • 先進行一次非同步的檢查,如果實例已經存在則直接返回,避免了每次調用都進行同步的性能開銷。
  • 當第一次檢查發現實例不存在時,進入同步代碼塊,再次檢查實例是否存在,這是為了防止多個線程同時通過第一次檢查,從而保證只創建一個實例。
  • 使用 volatile 關鍵字是為了禁止指令重排序,防止在實例還未完全初始化時就被其他線程使用。
6. 靜態內部類(線程安全)
public class Singleton {private Singleton() {}// 靜態內部類,在類加載時創建實例private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonHolder.INSTANCE;}
}

解釋

  • 利用靜態內部類的特性,當外部類被加載時,靜態內部類不會被加載,只有當調用 getInstance() 方法時,才會加載靜態內部類并創建實例,實現了懶加載。
  • 由于類加載過程是線程安全的,所以這種方式也是線程安全的。
7. 枚舉(線程安全)
public enum Singleton {INSTANCE;// 可以添加其他方法public void doSomething() {System.out.println("Doing something...");}
}

解釋

  • 枚舉類型的實例是由 Java 虛擬機保證線程安全和唯一性的,是實現單例模式的最佳方式之一。
  • 枚舉還可以防止反序列化重新創建新的對象,避免了單例模式被破壞的問題。

綜上所述,推薦使用雙重檢查鎖定、靜態內部類或枚舉的方式來實現線程安全的單例模式,它們在性能和線程安全方面都有較好的表現。

在 Java 中,使用枚舉實現線程安全的單例模式是一種簡單、高效且安全的方式。以下為你詳細介紹如何正確使用枚舉實現單例模式,包含原理、代碼示例、調用示例及優勢說明。

實現原理

Java 的枚舉類型本質上是一種特殊的類,它的實例是全局唯一且在類加載時由 JVM 保證線程安全地創建。這意味著枚舉實例的創建過程是線程安全的,并且不會被反射或反序列化破壞,非常適合用于實現單例模式。

代碼示例
// 定義一個枚舉類型作為單例類
public enum Singleton {// 定義單例實例INSTANCE;// 可以在枚舉中添加其他方法public void doSomething() {System.out.println("單例實例正在執行操作...");}
}
調用示例
public class Main {public static void main(String[] args) {// 獲取單例實例Singleton singleton = Singleton.INSTANCE;// 調用單例實例的方法singleton.doSomething();// 驗證單例的唯一性Singleton anotherSingleton = Singleton.INSTANCE;System.out.println("兩個實例是否相同: " + (singleton == anotherSingleton));}
}
代碼解釋
  1. 枚舉定義public enum Singleton 定義了一個名為 Singleton 的枚舉類型。
  2. 單例實例INSTANCE 是枚舉類型的一個實例,由于枚舉類型的特性,它是全局唯一的。
  3. 方法定義doSomething() 是一個自定義的方法,用于演示單例實例可以執行的操作。
  4. 調用單例:在 Main 類的 main 方法中,通過 Singleton.INSTANCE 獲取單例實例,并調用其 doSomething() 方法。
  5. 驗證唯一性:通過比較兩個獲取到的實例是否相同,驗證了單例的唯一性。
優勢
  • 線程安全:枚舉實例的創建由 JVM 保證線程安全,無需額外的同步措施。
  • 防止反射攻擊:Java 的反射機制無法實例化枚舉類型,避免了通過反射破壞單例的問題。
  • 防止反序列化破壞:枚舉類型在反序列化時,JVM 會確保返回的是原有的枚舉實例,而不是創建新的實例,保證了單例的唯一性。
總結

使用枚舉實現單例模式簡潔、安全,并且能有效避免多線程環境下的各種問題。在實際開發中,若需要實現單例模式,推薦優先考慮使用枚舉方式。

附錄:雙重檢查鎖定的詳細解釋

雙重檢查鎖定是一種用于實現線程安全的單例模式的方法。它的基本思路是在獲取單例實例時,先進行一次非同步的檢查,如果實例還未創建,再進入同步塊進行第二次檢查并創建實例,以此來減少同步帶來的性能開銷。以下是雙重檢查鎖定實現單例模式的基本代碼框架:

public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton();}}}return instance;}
}

在上述代碼中,如果不使用 volatile 關鍵字修飾 instance 變量,可能會出現實例還未完全初始化就被其他線程使用的問題,這主要和 Java 中的指令重排序有關。

指令重排序原理
在 Java 中,為了提高程序的執行效率,編譯器和處理器會對指令進行重排序。instance = new Singleton(); 這行代碼在底層實際上會分為三個步驟:

  1. 分配內存空間:為 Singleton 對象分配內存。
  2. 初始化對象:調用 Singleton 的構造函數,對對象進行初始化。
  3. 將引用指向對象:將 instance 引用指向剛分配的內存地址。

正常情況下,步驟的執行順序是 1 -> 2 -> 3。但由于指令重排序,步驟 2 和步驟 3 的執行順序可能會被交換,變成 1 -> 3 -> 2。
假設線程 A 進入同步塊創建 Singleton 實例,由于指令重排序,線程 A 先執行了步驟 1 和步驟 3,此時 instance 已經指向了分配的內存地址,但對象還未完成初始化(步驟 2 未執行)。
這時,線程 B 執行第一次檢查 if (instance == null),發現 instance 不為 null,就會直接返回 instance,并使用這個未完全初始化的對象,從而導致程序出現錯誤。
當使用 volatile 關鍵字修飾 instance 變量時,volatile 會禁止指令重排序,保證步驟 2 一定在步驟 3 之前執行。即保證對象在初始化完成后,instance 引用才會指向該對象。以下是添加 volatile 關鍵字后的代碼:

public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次檢查synchronized (Singleton.class) {if (instance == null) { // 第二次檢查instance = new Singleton();}}}return instance;}
}

在這個代碼中,volatile 關鍵字確保了在 instance 引用指向對象時,對象已經完成了初始化。這樣,當其他線程通過第一次檢查發現 instance 不為 null 時,獲取到的就是一個已經完全初始化的對象,避免了使用未完全初始化的實例而導致的問題。

綜上所述,volatile 關鍵字在雙重檢查鎖定的單例模式中是必不可少的,它通過禁止指令重排序保證了單例實例在被其他線程使用時已經完全初始化。

CAS

在 Java 中,CAS(Compare-And-Swap)是一種實現并發算法時常用到的技術,它是一種無鎖的原子操作,被廣泛應用于多線程編程中以實現高效的并發控制。下面將從基本概念、原理、在 Java 中的實現、優缺點等方面詳細分析說明 Java 中的 CAS。

基本概念

CAS 是一種樂觀鎖的實現機制,它假設在并發環境下,大多數情況下不會發生沖突,因此在進行操作時不會加鎖。CAS 操作包含三個操作數:內存位置(V)、預期原值(A)和新值(B)。當且僅當內存位置 V 的值等于預期原值 A 時,才會將該位置的值更新為新值 B;否則,不做任何操作。一般情況下,“更新”是一個原子操作,這個過程是不可被中斷的。

原理

CAS 操作是由 CPU 提供的原子指令來實現的,不同的 CPU 架構可能有不同的指令來支持 CAS 操作,例如 x86 架構中的 cmpxchg 指令。Java 中的 CAS 操作是基于底層的 CPU 指令實現的,因此具有很高的性能。

CAS 的基本流程如下:

  1. 讀取內存位置 V 的當前值。
  2. 比較該值是否等于預期原值 A。
  3. 如果相等,則將內存位置 V 的值更新為新值 B;如果不相等,則表示有其他線程已經修改了該位置的值,當前操作失敗,通常會重試或者放棄操作。
在 Java 中的實現

在 Java 中,java.util.concurrent.atomic 包提供了一系列基于 CAS 實現的原子類,例如 AtomicIntegerAtomicLongAtomicReference 等,這些類可以在多線程環境下安全地進行原子操作。

以下是一個使用 AtomicInteger 的示例:

import java.util.concurrent.atomic.AtomicInteger;public class CASTest {public static void main(String[] args) {// 創建一個 AtomicInteger 對象,初始值為 0AtomicInteger atomicInteger = new AtomicInteger(0);// 獲取當前值int expectedValue = atomicInteger.get();// 新值int newValue = expectedValue + 1;// 使用 CAS 操作更新值boolean success = atomicInteger.compareAndSet(expectedValue, newValue);if (success) {System.out.println("CAS 操作成功,新值為: " + atomicInteger.get());} else {System.out.println("CAS 操作失敗,當前值為: " + atomicInteger.get());}}
}

在上述示例中,compareAndSet 方法就是一個 CAS 操作,它會比較 atomicInteger 的當前值是否等于 expectedValue,如果相等則將其更新為 newValue,并返回 true;否則返回 false

CAS 在 JUC 中的應用

Java 并發包(JUC,java.util.concurrent)中的很多類都使用了 CAS 技術來實現高效的并發控制,例如 ReentrantLock 中的 AbstractQueuedSynchronizer(AQS)、ConcurrentHashMap 等。

ReentrantLock 為例,它的底層使用了 AQS,而 AQS 在更新同步狀態時就使用了 CAS 操作,保證了在多線程環境下對同步狀態的原子更新,避免了使用傳統的鎖機制帶來的性能開銷。

優缺點
優點
  1. 無鎖并發:CAS 是一種無鎖的并發機制,避免了傳統鎖機制帶來的線程阻塞和上下文切換開銷,因此在高并發場景下具有更好的性能。
  2. 原子性:CAS 操作是原子的,由 CPU 指令保證,不會被其他線程中斷,確保了數據的一致性。
  3. 樂觀鎖思想:基于樂觀鎖的思想,假設大多數情況下不會發生沖突,減少了鎖的使用,提高了并發性能。
缺點
  1. ABA 問題:CAS 操作只比較值是否相等,當一個值從 A 變為 B 再變回 A 時,CAS 操作會認為值沒有發生變化,但實際上中間已經發生了修改。在某些場景下,這種變化可能會對程序的邏輯產生影響。可以使用 AtomicStampedReferenceAtomicMarkableReference 來解決 ABA 問題。
  2. 自旋開銷:當 CAS 操作失敗時,通常會進行重試(自旋),如果長時間自旋會消耗大量的 CPU 資源,特別是在競爭激烈的情況下。
  3. 只能保證一個變量的原子操作:CAS 只能保證對一個變量的原子操作,如果需要對多個變量進行原子操作,CAS 就無法直接滿足需求,可能需要使用鎖或者其他并發機制。

綜上所述,CAS 是 Java 中一種非常重要的并發技術,它在提高并發性能方面具有很大的優勢,但也存在一些缺點,在使用時需要根據具體的場景進行權衡和選擇。
多線程代碼案例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;// 定義一個任務類,用于對計數器進行自增操作
class IncrementTask implements Runnable {private final AtomicInteger counter;private final int incrementCount;public IncrementTask(AtomicInteger counter, int incrementCount) {this.counter = counter;this.incrementCount = incrementCount;}@Overridepublic void run() {for (int i = 0; i < incrementCount; i++) {// 循環進行 CAS 操作,直到成功while (true) {int current = counter.get();int next = current + 1;// 使用 CAS 操作嘗試更新計數器的值if (counter.compareAndSet(current, next)) {break;}}}}
}public class CASTest {public static void main(String[] args) {// 創建一個初始值為 0 的 AtomicInteger 計數器AtomicInteger counter = new AtomicInteger(0);// 定義每個線程需要自增的次數int incrementCount = 10000;// 定義線程的數量int threadCount = 10;// 創建一個固定大小的線程池ExecutorService executorService = Executors.newFixedThreadPool(threadCount);// 提交任務到線程池for (int i = 0; i < threadCount; i++) {executorService.submit(new IncrementTask(counter, incrementCount));}// 關閉線程池并等待所有任務完成executorService.shutdown();while (!executorService.isTerminated()) {// 等待所有任務完成}// 輸出最終的計數器值System.out.println("Final counter value: " + counter.get());}
}
附錄:unsafe的底層C++

在 Java 里,CAS(Compare-And-Swap)操作主要是借助 java.util.concurrent.atomic 包下的原子類(像 AtomicIntegerAtomicLong 等)來達成的,而這些原子類中的 CAS 操作大多是依靠 native 方法實現的。下面以 AtomicInteger 類為例,深入探討其 native 方法的具體內容并加以分析。

  1. Java 層面的 CAS 方法調用
    AtomicInteger 類中,compareAndSet 方法是一個典型的 CAS 操作方法,其源碼如下:
public class AtomicInteger extends Number implements java.io.Serializable {private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();private static final long VALUE;static {try {VALUE = U.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (ReflectiveOperationException e) {throw new Error(e);}}private volatile int value;public final boolean compareAndSet(int expectedValue, int newValue) {return U.compareAndSetInt(this, VALUE, expectedValue, newValue);}
}

從這段代碼可知,compareAndSet 方法調用了 jdk.internal.misc.Unsafe 類的 compareAndSetInt 方法,此方法是一個 native 方法。

  1. Unsafe 類的 native 方法
    Unsafe 類提供了眾多底層操作的 native 方法,compareAndSetInt 方法便是其中之一,它的聲明如下:
public final class Unsafe {// 其他代碼...public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);// 其他代碼...
}
  • 參數說明
    • o:表示要操作的對象。
    • offset:表示對象中要操作的字段的內存偏移量。
    • expected:表示預期的舊值。
    • x:表示要設置的新值。
  1. native 方法的具體實現(以 HotSpot JVM 為例)
    Unsafe 類的 native 方法實現在 HotSpot JVM 的源碼里,對應的 C++ 代碼文件一般是 unsafe.cppcompareAndSetInt 方法的具體實現會依據不同的操作系統和硬件架構有所差異,下面以 x86 架構為例:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p = JNIHandles::resolve(obj);jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

上述代碼調用了 Atomic::cmpxchg 函數,該函數是一個原子操作函數,其實現如下:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {int mp = os::is_MP();__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)": "=a" (exchange_value): "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp): "cc", "memory");return exchange_value;
}
  • 代碼分析
    • os::is_MP():用于判斷當前系統是否為多處理器系統。
    • LOCK_IF_MP(%4):如果是多處理器系統,會在 cmpxchgl 指令前添加 lock 前綴,以此確保該操作的原子性。lock 前綴能夠保證在執行該指令期間,處理器會鎖定總線,防止其他處理器對同一內存地址進行操作。
    • cmpxchgl %1,(%3):這是 x86 架構的匯編指令,cmpxchgl 是比較并交換指令。它會比較 eax 寄存器的值(即 compare_value)和內存地址 dest 處的值,如果相等,則將 exchange_value 寫入 dest 地址,并將 ZF(零標志位)置為 1;如果不相等,則將 dest 地址處的值加載到 eax 寄存器,并將 ZF 置為 0。
  1. 總結
  • Java 中的 CAS 操作通過 Unsafe 類的 native 方法實現,這些 native 方法會調用底層操作系統和硬件提供的原子指令(如 x86 架構的 cmpxchg 指令)。
  • 在多處理器系統中,為保證操作的原子性,會在原子指令前添加 lock 前綴,鎖定總線。
  • CAS 操作是一種無鎖的原子操作,性能優于傳統的鎖機制,不過在高競爭場景下可能會出現自旋等待,進而影響性能。

原子引用

帶版本號的CAS,專門解決ABA問題

常見鎖的提法

公平與非公平鎖
  1. 公平鎖需要先來后到
  2. 非公平鎖相反
  3. 大多數情況下用非公平鎖,讓小任務先跑
樂觀鎖與悲觀鎖
  1. 樂觀鎖默認資源搶占不嚴重
  2. 悲觀鎖相反
可重入鎖
  1. 拿到一次鎖的可以再次自動獲得鎖
偏向鎖

偏向鎖是 Java 虛擬機(JVM)為了減少無競爭情況下的鎖開銷而引入的一種鎖優化機制,主要應用于 synchronized 關鍵字所修飾的同步塊場景,旨在提升單線程環境下的性能。下面將從基本概念、原理、優缺點、使用場景、JVM 相關參數幾個方面詳細介紹偏向鎖。

基本概念
在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的核心思想是,當一個線程第一次獲得鎖時,會在對象頭中記錄該線程的 ID,這個過程稱為偏向。此后,該線程再次進入和退出同步塊時,無需進行任何同步操作,如加鎖、解鎖,從而提高了單線程環境下的性能。

原理
偏向鎖的實現主要涉及對象頭和鎖記錄的變化,其具體原理如下:

  1. 偏向鎖的初始化:當一個對象被創建時,它的對象頭中標記位為“01”,表示未鎖定狀態,此時偏向鎖標志位為“0”,表示該對象還未偏向任何線程。
  2. 線程首次獲取鎖:當有線程第一次訪問同步塊并獲取鎖時,JVM 會使用 CAS(Compare-And-Swap)操作將該線程的 ID 記錄在對象頭的偏向線程 ID 字段中,并將偏向鎖標志位設置為“1”。這個過程一旦成功,該線程就獲得了偏向鎖,以后該線程再次進入這個同步塊時,無需進行任何同步操作,直接進入同步塊執行代碼。
  3. 其他線程競爭鎖:當有其他線程嘗試獲取該鎖時,會發現對象頭中的偏向線程 ID 不是自己的線程 ID,此時偏向鎖會被撤銷。撤銷偏向鎖需要等待全局安全點(Safe Point),在這個時間點上所有線程都處于暫停狀態。撤銷后,對象頭會恢復到未鎖定狀態或者升級為輕量級鎖,具體取決于競爭情況。

優缺點
優點

  • 性能提升顯著:在單線程環境下,偏向鎖避免了傳統鎖機制中的加鎖和解鎖操作,減少了同步開銷,大大提高了程序的執行效率。
  • 實現簡單:偏向鎖的實現相對簡單,只需要在對象頭中記錄線程 ID 即可,不需要額外的數據結構來維護鎖狀態。

缺點

  • 撤銷成本較高:當出現鎖競爭時,需要撤銷偏向鎖,這需要等待全局安全點,會導致一定的性能開銷。
  • 不適合多線程競爭場景:如果一個鎖經常被多個線程競爭,偏向鎖的撤銷和升級操作會頻繁發生,反而會降低性能。

使用場景
偏向鎖適用于大多數情況下只有一個線程訪問同步塊的場景,例如:

  • 單例模式中的同步塊:在單例模式的實現中,通常會使用 synchronized 關鍵字來保證線程安全。如果單例對象的創建和使用主要由一個線程完成,偏向鎖可以顯著提高性能。
  • 單線程循環中的同步操作:在一些單線程的循環代碼中,如果存在同步塊,使用偏向鎖可以避免不必要的同步開銷。

JVM 相關參數
在 JVM 中,可以通過以下參數來控制偏向鎖的行為:

  • -XX:+UseBiasedLocking:開啟偏向鎖,這是 JDK 6 及以后版本的默認設置。
  • -XX:-UseBiasedLocking:關閉偏向鎖。
  • -XX:BiasedLockingStartupDelay=0:設置偏向鎖的啟動延遲為 0,即 JVM 啟動后立即啟用偏向鎖。默認情況下,JVM 會有一個 4 秒的啟動延遲,以避免在 JVM 啟動初期出現大量的偏向鎖撤銷操作。

綜上所述,偏向鎖是一種針對單線程環境的鎖優化機制,通過減少無競爭情況下的鎖開銷來提高程序性能。但在多線程競爭頻繁的場景下,需要謹慎使用,必要時可以通過 JVM 參數進行調整。

自旋鎖

這個很好理解,不多說

表鎖行鎖間隙鎖及MVCC

主要針對mysql的鎖機制

在 MySQL 中,不同的存儲引擎支持不同的鎖機制,其中 InnoDB 引擎支持表鎖、行鎖和間隙鎖,下面詳細介紹哪些操作會觸發這些鎖:

  1. 表鎖
    表鎖會鎖定整個表,在鎖定期間,其他事務無法對該表進行寫操作,讀操作可能會受到不同程度的限制,具體取決于鎖的類型。

顯式加表鎖操作

  • LOCK TABLES 語句:這是一種顯式對表加鎖的方式,可以指定不同的鎖模式,如讀鎖(READ)和寫鎖(WRITE)。
    • 讀鎖示例
-- 給 table_name 表加讀鎖
LOCK TABLES table_name READ;
-- 其他事務可以讀取該表,但不能寫入
-- 當前事務可以讀取該表,但不能對其他表進行操作
-- 釋放鎖
UNLOCK TABLES;
- **寫鎖示例**:
-- 給 table_name 表加寫鎖
LOCK TABLES table_name WRITE;
-- 其他事務不能讀取和寫入該表
-- 當前事務可以讀寫該表,但不能對其他表進行操作
-- 釋放鎖
UNLOCK TABLES;
  • ALTER TABLE 語句:當執行 ALTER TABLE 語句修改表結構時,MySQL 會自動對表加寫鎖,以防止其他事務在修改過程中對表進行讀寫操作,確保數據的一致性。例如:
ALTER TABLE table_name ADD COLUMN new_column INT;

隱式加表鎖操作
在某些存儲引擎(如 MyISAM)中,默認使用表鎖。當執行 INSERTUPDATEDELETE 等寫操作時,會自動對表加寫鎖;執行 SELECT 操作時,會自動對表加讀鎖。不過 InnoDB 引擎通常不會因為簡單的讀寫操作隱式加表鎖,除非在特定的隔離級別或使用特定的語句。

  1. 行鎖
    行鎖會鎖定表中的某一行或多行,其他事務可以對未鎖定的行進行讀寫操作,從而提高并發性能。InnoDB 引擎默認使用行鎖。

基于索引的 UPDATEDELETE 操作
當使用 UPDATEDELETE 語句時,如果使用了索引進行條件過濾,并且索引是唯一索引或主鍵索引,MySQL 會使用行鎖鎖定匹配的行。例如:

-- 假設 id 是主鍵
UPDATE table_name SET column1 = 'new_value' WHERE id = 1;
DELETE FROM table_name WHERE id = 2;

基于索引的 SELECT ... FOR UPDATE 操作
SELECT ... FOR UPDATE 語句會對查詢結果中的行加排他鎖,防止其他事務對這些行進行修改。例如:

-- 假設 id 是主鍵
SELECT * FROM table_name WHERE id BETWEEN 1 AND 10 FOR UPDATE;

插入操作(針對唯一索引沖突)
當插入數據時,如果違反了唯一索引的約束,MySQL 會對沖突的行加鎖。例如:

-- 假設 unique_column 是唯一索引
INSERT INTO table_name (unique_column) VALUES ('duplicate_value');

間隙鎖
間隙鎖是 InnoDB 引擎在可重復讀(REPEATABLE READ)隔離級別下為了防止幻讀而引入的一種鎖機制,它會鎖定索引記錄之間的間隙,防止其他事務在這些間隙中插入新的記錄。

范圍查詢
當使用 UPDATEDELETESELECT ... FOR UPDATE 語句進行范圍查詢時,如果查詢條件使用了索引,并且沒有完全匹配的記錄,MySQL 會使用間隙鎖鎖定查詢范圍對應的間隙。例如:

-- 假設 id 是索引
SELECT * FROM table_name WHERE id BETWEEN 10 AND 20 FOR UPDATE;

如果表中 id 在 10 到 20 之間沒有記錄,MySQL 會鎖定這個區間的間隙,防止其他事務插入 id 在 10 到 20 之間的記錄。

插入操作引發的間隙鎖
在某些情況下,插入操作也可能會觸發間隙鎖。例如,當插入數據時,如果需要檢查唯一索引是否沖突,MySQL 可能會對相鄰的間隙加鎖。

需要注意的是,間隙鎖只在可重復讀隔離級別下生效,在讀已提交(READ COMMITTED)隔離級別下,MySQL 不會使用間隙鎖。
特別地,MVCC在可重復讀級別不能完全解決幻讀問題,只有串行化能完全解決

死鎖及排查

死鎖是指兩個或多個線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。下面為你介紹常見的死鎖排查方案。

  1. 日志分析
  • 原理:在代碼中添加詳細的日志記錄,記錄鎖的獲取和釋放操作,以及關鍵的業務步驟。當發生死鎖時,通過查看日志可以了解各個線程的執行順序和鎖的持有情況,從而找出可能導致死鎖的原因。
  • 操作步驟
    • 在代碼里,在獲取鎖之前和釋放鎖之后添加日志,例如:
import java.util.logging.Level;
import java.util.logging.Logger;public class LoggingExample {private static final Logger LOGGER = Logger.getLogger(LoggingExample.class.getName());private final Object lock1 = new Object();private final Object lock2 = new Object();public void method1() {LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock1", Thread.currentThread().getName());synchronized (lock1) {LOGGER.log(Level.INFO, "Thread {0} has acquired lock1", Thread.currentThread().getName());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock2", Thread.currentThread().getName());synchronized (lock2) {LOGGER.log(Level.INFO, "Thread {0} has acquired lock2", Thread.currentThread().getName());}LOGGER.log(Level.INFO, "Thread {0} has released lock2", Thread.currentThread().getName());}LOGGER.log(Level.INFO, "Thread {0} has released lock1", Thread.currentThread().getName());}public void method2() {LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock2", Thread.currentThread().getName());synchronized (lock2) {LOGGER.log(Level.INFO, "Thread {0} has acquired lock2", Thread.currentThread().getName());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}LOGGER.log(Level.INFO, "Thread {0} is trying to acquire lock1", Thread.currentThread().getName());synchronized (lock1) {LOGGER.log(Level.INFO, "Thread {0} has acquired lock1", Thread.currentThread().getName());}LOGGER.log(Level.INFO, "Thread {0} has released lock1", Thread.currentThread().getName());}LOGGER.log(Level.INFO, "Thread {0} has released lock2", Thread.currentThread().getName());}
}
- 當程序出現死鎖后,查看日志文件,分析各個線程的鎖獲取和釋放順序,找出循環等待的情況。
  1. jstack 工具
  • 原理jstack 是 JDK 自帶的一個命令行工具,用于生成 Java 虛擬機(JVM)中線程的快照。線程快照是當前 JVM 內每一條線程正在執行的方法堆棧的集合,通過分析這些堆棧信息,可以找出處于死鎖狀態的線程及其持有和等待的鎖。
  • 操作步驟
    a. 使用 jps 命令查找 Java 進程的 PID(進程 ID),例如:
jps
b. 使用 `jstack` 命令生成指定進程的線程快照,例如:
jstack <PID> > thread_dump.txt
c. 打開生成的 `thread_dump.txt` 文件,查找包含 “`Found one Java-level deadlock`” 的信息,下面會詳細列出死鎖的線程及其持有和等待的鎖。
  1. VisualVM 工具
  • 原理:VisualVM 是一個可視化的 Java 性能分析工具,它可以監控 Java 應用程序的性能、線程狀態等信息。通過 VisualVM 的線程分析功能,可以直觀地查看各個線程的狀態,找出死鎖線程。
  • 操作步驟
    a. 啟動 VisualVM 工具。
    b. 在左側的應用程序列表中選擇要分析的 Java 進程。
    c. 切換到 “線程” 標簽頁,查看線程的狀態。如果存在死鎖,VisualVM 會在界面上標記出死鎖線程,并提供詳細的線程堆棧信息。
  1. 數據庫死鎖排查(以 MySQL 為例)
  • 原理:數據庫中的死鎖通常是由于多個事務同時競爭資源(如鎖表、鎖行)導致的。MySQL 提供了一些系統表和命令來幫助排查死鎖問題。
  • 操作步驟
    • 查看死鎖日志:在 MySQL 的錯誤日志文件中查找包含 “Deadlock found when trying to get lock” 的信息,日志中會詳細記錄死鎖發生的時間、涉及的事務和 SQL 語句。
    • 使用 SHOW ENGINE INNODB STATUS 命令:該命令可以顯示 InnoDB 存儲引擎的狀態信息,其中包含了最近一次死鎖的詳細信息,如死鎖發生的時間、涉及的事務 ID、鎖的類型等。例如:
SHOW ENGINE INNODB STATUS;
  1. 代碼靜態分析工具
  • 原理:代碼靜態分析工具可以在不運行代碼的情況下,對代碼進行分析,找出可能存在的死鎖隱患。這些工具會檢查代碼中的鎖獲取和釋放邏輯,識別出可能導致死鎖的代碼模式。
  • 常見工具:FindBugs、SonarQube 等。
  • 操作步驟
    • 安裝并配置代碼靜態分析工具。
    • 使用工具對代碼進行掃描,工具會生成分析報告,指出可能存在死鎖隱患的代碼位置和原因。

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

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

相關文章

【Unity】從父對象中獲取子對象組件的方式

1.GetComponentInChildren 用于獲取對與指定組件或游戲對象的任何子級相同的游戲對象上的組件類型的引用。 該方法在Unity腳本API的聲明格式為&#xff1a; public T GetComponentInChildren(bool includeInactive false) includeInactive參數&#xff08;可選&#xff09…

Redis性能優化

1.是否使用復雜度過高的命令 首先&#xff0c;第一步&#xff0c;你需要去查看一下 Redis 的慢日志&#xff08;slowlog&#xff09;。 Redis 提供了慢日志命令的統計功能&#xff0c;它記錄了有哪些命令在執行時耗時比較久。 查看 Redis 慢日志之前&#xff0c;你需要設置慢…

baigeiRSA

baigeiRSA 打開附件有兩個&#xff1a; 1.import libnumfrom Crypto.Util import numberfrom secret import flag?size 128e 65537p number.getPrime(size)q number.getPrime(size)n p*q?m libnum.s2n(flag)c pow(m, e, n)?print(n %d % n)print(c %d % c)??2.n…

【csp-j學習完C++語法后,如何進階學習C++算法和數據結構?】

在掌握了 CSP - J 的 C 語法基礎后&#xff0c;接下來的進階學習需要系統地掌握各類算法和數據結構知識&#xff0c;并通過大量練習來鞏固和提高應用能力。以下是一份詳細的進階學習規劃&#xff1a; 第一階段&#xff1a;基礎算法學習&#xff08;1 - 2 個月&#xff09; 排…

QT中解決使用QCustomplot繪制高速大量數據時頻譜圖卡頓問題

[&#xff01;&#xff01;&#xff01;核心方法&#xff01;&#xff01;&#xff01;] 使用帶參數的replot()函數繪制m_pCustomPlot>replot(QCustomPlot::rpQueuedReplot) 1. replot() 方法 void QCustomPlot::replot(QCustomPlot::RefreshPriority refreshPriority rp…

【AI】卷積神經網絡CNN

不定期更新&#xff0c;建議關注收藏點贊。 目錄 零碎小組件經驗總結早期的CNN 零碎小組件 全連接神經網絡 目前已經被替代。 每個神經元都有參與&#xff0c;但由于數據中的特征點變化大&#xff0c;全連接神經網絡把所有數據特征都學習了&#xff0c;故效果不好。感受野&…

YOLOv11-ultralytics-8.3.67部分代碼閱讀筆記-downloads.py

downloads.py ultralytics\utils\downloads.py 目錄 downloads.py 1.所需的庫和模塊 2.def is_url(url, checkFalse): 3.def delete_dsstore(path, files_to_delete(".DS_Store", "__MACOSX")): 4.def zip_directory(directory, compressTrue, ex…

微信小程序~電器維修系統小程序

博主介紹&#xff1a;?程序猿徐師兄、8年大廠程序員經歷。全網粉絲15w、csdn博客專家、掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域和畢業項目實戰? &#x1f345;文末獲取源碼聯系&#x1f345; &#x1f447;&#x1f3fb; 精彩專欄推薦訂閱&#x1f447;…

VDN 微服務架構搭建篇(三)基于 Nacos 的 Spring Cloud Gateway 動態路由管理

VDN 微服務架構搭建篇&#xff08;三&#xff09;&#xff1a;基于 Nacos 的 Spring Cloud Gateway 動態路由管理 在微服務架構中&#xff0c;網關 是整個系統的入口&#xff0c;負責 流量管理、請求路由、安全控制等關鍵功能。 Spring Cloud Gateway 作為 Spring 生態官方推薦…

LLAMA-Factory安裝教程(解決報錯cannot allocate memory in static TLS block的問題)

步驟一&#xff1a; 下載基礎鏡像 # 配置docker DNS vi /etc/docker/daemon.json # daemon.json文件中 { "insecure-registries": ["https://swr.cn-east-317.qdrgznjszx.com"], "registry-mirrors": ["https://docker.mirrors.ustc.edu.c…

Java高頻面試之SE-18

hello啊&#xff0c;各位觀眾姥爺們&#xff01;&#xff01;&#xff01;本baby今天又來了&#xff01;哈哈哈哈哈嗝&#x1f436; BIO NIO AIO的區別&#xff1f; 在 Java 網絡編程中&#xff0c;BIO、NIO 和 AIO 是三種不同的 I/O 模型&#xff0c;它們的核心區別在于 阻塞…

藍橋杯刷題DAY3:Horner 法則 前綴和+差分數組 貪心

所謂刷題&#xff0c;最重要的就是細心 &#x1f4cc; 題目描述 在 X 進制 中&#xff0c;每一數位的進制不固定。例如&#xff1a; 最低位 采用 2 進制&#xff0c;第二位 采用 10 進制&#xff0c;第三位 采用 8 進制&#xff0c; 則 X 進制數 321 的十進制值為&#xff…

BUU24 [GXYCTF2019]BabyUpload 1

開局上傳文件 上傳muma.php 上傳.htaccess文件也被打回 再次求助互聯網&#xff0c;才發現這提示給的多么明顯&#xff0c;上傳.htaccess文件是檢查文件類型&#xff08;Contnet-Type&#xff09;&#xff0c;上傳muma.php是檢查后綴里頭有沒有ph &#xff0c;檢查文件類型那…

RabbitMQ 從入門到精通:從工作模式到集群部署實戰(三)

文章目錄 使用CLI管理RabbitMQrabbitmqctlrabbitmq-queuesrabbitmq-diagnosticsrabbitmq-pluginsrabbitmq-streamsrabbitmq-upgraderabbitmqadmin 使用CLI管理RabbitMQ RabbitMQ CLI 工具需要安裝兼容的 Erlang/OTP版本。 這些工具假定系統區域設置為 UTF-8&#xff08;例如en…

3.攻防世界 weak_auth

題目描述提示 是一個登錄界面&#xff0c;需要密碼登錄 進入題目頁面如下 弱口令密碼爆破 用1 or 1 #試試 提示用admin登錄 則嘗試 用戶名admin密碼&#xff1a;123456 直接得到flag 常用弱口令密碼&#xff08;可復制&#xff09; 用戶名 admin admin-- admin or -- admin…

優化深度神經網絡

訓練集、開發集(驗證集)、測試集 偏差與方差 正則化 L2正則 Dropout 隨機丟棄部分神經元輸入&#xff0c;經常用于計算機視覺的神經網絡內&#xff0c;因為通常沒有足夠的訓練數據&#xff0c;很容易出現過擬合的問題 數據增強 訓練集規一化 可以使其圖像更均勻&#xff0c;…

【玩轉 Postman 接口測試與開發2_018】第14章:利用 Postman 初探 API 安全測試

《API Testing and Development with Postman》最新第二版封面 文章目錄 第十四章 API 安全測試1 OWASP API 安全清單1.1 相關背景1.2 OWASP API 安全清單1.3 認證與授權1.4 破防的對象級授權&#xff08;Broken object-level authorization&#xff09;1.5 破防的屬性級授權&a…

Spring @PropertySource:讓你的應用配置更加模塊化和可維護

PropertySource注解在Spring中的作用&#xff0c;就像是給Spring應用配了一個“外部配置箱”。 想象一下&#xff0c;你在開發一個Spring應用時&#xff0c;有很多配置信息需要設置&#xff0c;比如數據庫的連接信息、應用的某些功能開關等。如果這些信息都硬編碼在代碼中&…

RK3576——USB3.2 OTG無法識別到USB設備

問題&#xff1a;使用硬盤接入到OTG接口無熱插拔信息&#xff0c;接入DP顯示屏無法正常識別到顯示設備&#xff0c;但是能通過RKDdevTool工具燒錄系統。 問題分析&#xff1a;由于熱插拔功能實現是靠HUSB311芯片完成的&#xff0c;因此需要先確保HUSB311芯片驅動正常工作。 1. …

docker-compose 配置nginx

前言 前端打包的dist文件在宿主機&#xff0c;nginx運行在docker-compose 問題 nginx.conf 在本地配置可以生效&#xff0c;但是鏈接到容器就報錯 基于本地的nginx運行&#xff0c;本地nginx.conf 如下 server {listen 8081;location / {root /usr/local/software/testweb/…