技術面:Java并發(線程同步、死鎖、多線程編排)
線程同步的方式有哪些?
線程同步
線程同步,是多線程編程中的一種機制,用于協調多個線程的執行順序,確保它們在共享資源或關鍵操作上按照預定的規則運行,避免因并發訪問導致的數據不一致、競態條件(Race Condition)等問題。
線程同步的方式有哪些?
synchronized關鍵字,通過 JVM 內置的鎖機制實現線程同步,確保同一時刻只有一個線程訪問共享資源。既可以修飾實例方法也可以修飾靜態方法,也可以鎖代碼塊和鎖住某個具體的實例對象。
public synchronized void method() { ... } // 實例方法鎖(this)
public static synchronized void method() { ... } // 類方法鎖(Class 對象)
// 鎖實例對象
synchronized (lockObject) {
// 同步代碼塊
}
ReentrantLock基于java.util.concurrent.locks.Lock接口實現的可重入互斥鎖,需顯式調用lock()和unlock()進行加鎖和解鎖。
支持公平鎖、可中斷鎖、超時鎖以及多條件變量(Condition),相比 synchronized 提供了更高的靈活性。
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 同步代碼
} finally {
lock.unlock();
}
Semaphore(信號量),允許多個線程同時訪問資源,但是限制訪問線程的數量。
Semaphore semaphore = new Semaphore(3); // 初始許可數為3
semaphore.acquire(); // 獲取許可
try {
// 同步代碼
} finally {
semaphore.release(); // 釋放許可
}
CountDownLatch,允許多個線程等待其他線程執行完畢之后再執行,用于線程間的協作。
public class LatchDemo {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 1; i <= threadCount; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 初始化完成");
latch.countDown(); // 子線程完成,計數器減 1
}, "線程-" + i).start();
}
latch.await(); // 主線程等待所有子線程完成
System.out.println("所有子線程完成,主線程繼續執行");
}
}
CyclicBarrier,多個線程互相等待,所有線程都到到屏障點后,再繼續執行。線程計數器可重置。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CyclicBarrierExample {
// 總和變量(線程安全)
private static int sum = 0;
// 線程池
private static final ExecutorService executor = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
// 定義需要等待的線程數量(5個)
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
// 所有線程到達屏障后執行的回調(匯總結果)
System.out.println("所有線程已完成計算,總和為: " + sum);
});
// 啟動5個線程
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
try {
// 模擬線程計算
int value = (int) (Math.random() * 100);
System.out.println(Thread.currentThread().getName() + " 計算值: " + value);
// 將計算結果累加到總和中
sum += value;
// 調用await()等待其他線程到達屏障
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
// 關閉線程池
executor.shutdown();
}
}
Phaser,和CyclicBarrier類似,但是支持更靈活的屏障操作,適用于復雜多階段任務,支持動態注冊/注銷參與者,并可控制各參與者的階段進度,并可以控制各個參與者的到達和離開。
Phaser phaser = new Phaser(1); // 初始參與線程
phaser.bulkRegister(3); // 動態注冊3個工作線程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int phase = 0; phase < 2; phase++) {
phaser.arriveAndAwaitAdvance(); // 階段1:等待所有線程完成階段1
// 執行階段2任務
}
phaser.arriveAndDeregister(); // 完成并注銷
}).start();
}
// 主線程等待所有線程完成
phaser.arriveAndAwaitAdvance();
- 其他,另外還有
volatile,這種能保證可見性和有序性,但不能保證原子性的關鍵字。以及基于CAS實現的Atomic類(無鎖同步),但是僅適用于簡單數據類型和部分操作(如 getAndAdd)。
死鎖
死鎖,通常是指在兩個或多個進程(或線程、事務)在執行過程中,因爭奪資源而陷入相互等待的狀態,導致所有進程都無法繼續執行。
什么情況下會產生死鎖?
產生死鎖的四個必要條件
- 互斥(Mutual Exclusion),一個資源只能被一個進程占用,其他進程必須等待其釋放。
- 占有并等待(Hold and Wait),進程在持有資源的同時,申請新的資源。
- 不可剝奪(No Preemption),資源只能由持有它的進程主動釋放,不能被強制剝奪。
- 循環等待(Circular Wait),存在一個進程環,每個進程都在等待下一個進程所持有的資源。

如何解決死鎖?
上面我們已經知道產生死鎖有四個必要條件,那解決死鎖,只需要破壞死鎖的這些必要條件即可。
一般從以下幾方面入手即可:
-
破壞“占有并等待”條件
- 進程或線程一次性申請所需的所有資源,否則不分配任何資源。(
可能導致資源利用率低,進程長期等待資源) - 進程或線程申請資源時,必須釋放已持有的所有資源。(
可能導致頻繁的資源釋放和重新申請,增加系統開銷)
- 進程或線程一次性申請所需的所有資源,否則不分配任何資源。(
-
破壞“不可剝奪”條件,允許系統強制回收資源。(
可能中斷進程的正常執行,導致數據不一致) -
破壞“循環等待”條件,要求進程或線程按順序申請資源。保證多個進程(線程)的執行順序相同即可避免循環等待。這是最常用的解決死鎖的方法。
例如:事務1的執行順序是:A->B->C,事務2的執行順序是:C->D->A,這種情況下就容易產生死鎖。因為事務1占用了A,等待C,但是事務2占用了C但是等待A。因此只需要把事務2的執行順序改成:A->D->C,這樣事務2在執行時,會發現事務1占用著A呢,因此事務會先不執行,等待事務1釋放A。

死鎖如何恢復?
回滾進程或線程,可以執行一個或多個進程(或線程)回滾到安全狀態,釋放資源。一般回滾時,要遵循按優先級選擇(優先級低的進程先回滾) 。按資源占用時間選擇(占用時間短的進程先回滾)。終止進程或線程,直接終止全部或部分死鎖進程(或線程),釋放資源。(可能導致數據丟失或事務不完整)資源剝奪,從某些進程或線程中強制回收資源分配給其他進程。超時機制,為進程或線程設置等待資源的超時時間,若超時則自動放棄請求并釋放已占資源。
數據庫中的死鎖
在操作數據庫時,如果有多個事務并發執行,也是可能發生死鎖的。當事務1持有資源A的鎖,但是嘗試獲取資源B的鎖,而事務2持有資源B的鎖,嘗試獲取資源A的鎖的時候,這時候就會發生死鎖的情況。
當數據庫發生死鎖的時候,會報出來如下的錯誤:
Error updating database. Cause: ERR-CODE: [TDDL-4614][ERR EXECUTE ON MYSQL]
Deadlock found when trying to get lock;
The error occurred while setting parameters### SQL:
update test_table set updated=now(),type_state = ? where test_num = 123
數據庫操作中如何避免死鎖?
一般對于數據庫的死鎖,主要是避免發生并發更新同一資源的操作。或者可以考慮保證操作的順序,比如多個事務都是先操作資源A、再操作資源B,這樣就能有效的避免死鎖。
還有一些其他優化措施:
減少事務持有鎖的時間:盡快提交或回滾事務。
鎖粒度控制:使用行級鎖而非表級鎖,減少資源競爭。
避免嵌套事務:減少循環等待的可能性。
多線程編排
在 Java 中,多線程的編排可以通過多種方式實現,主要涉及 線程池、同步機制、并發工具類 以及 任務協調工具(如 Future、CompletableFuture)等。
CompletableFuture怎么實現多線程異步編排?
CompletableFuture,提供了非常強大的Future的擴展功能,可以幫助我們簡化異步編程的復雜性,提供了函數式編程的能力,可以通過回調的方式處理計算結果,并且提供了轉換和組合CompletableFuture的方法。
我在【上一篇文章】提過CompletableFuture底層就是用ForkJoinPool來實現,那么CompletableFuture如何使用來實現多線程任務編排的呢?
單個任務
runAsync:無返回值
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
System.out.println("無返回值任務執行中");
});
supplyAsync:有返回值
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "異步任務結果";
});
System.out.println(future.get()); // 輸出: 異步任務結果
指定線程池
因為CompletableFuture默認底層是使用的ForkJoinPool.commonPool(),但是也是可以自定義線程池,配置線程的一些指定信息。
ExecutorService executor = Executors.newFixedThreadPool(2);
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return "自定義線程池任務";
}, executor);
兩個任務編排
thenApplyAsync:能接收上一次的執行結果,還可以有返回值
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenApplyAsync(result -> result + " World");// 運行結果 Hello World
thenRunAsync:不能接收上一次的執行結果,并且也沒返回值
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
return "Hello";
}).thenRunAsync(() -> {
System.out.println("after Hello World");
});
組合多個任務編排
- 串行組合(
thenCompose),可以將前一個任務的結果傳遞給下一個任務(鏈式依賴)
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
.thenCompose(result -> CompletableFuture.supplyAsync(() -> result + " World"));
System.out.println(future.get()); // 輸出: Hello World
并行組合(thenCombine),將兩個獨立任務的結果進行合并
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> "World");
future1.thenCombine(future2, (result1, result2) -> result1 + " " + result2)
.thenAccept(System.out::println); // 輸出: Hello World
多任務并行(allOf / anyOf)
allOf:等待所有任務完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
CompletableFuture.runAsync(() -> System.out.println("Task 1")),
CompletableFuture.runAsync(() -> System.out.println("Task 2"))
);
allFutures.get(); // 等待所有任務完成
anyOf:任一任務完成即觸發
CompletableFuture<Object> anyFuture = CompletableFuture.anyOf(
CompletableFuture.supplyAsync(() -> "Task 1"),
CompletableFuture.supplyAsync(() -> "Task 2")
);
System.out.println(anyFuture.get()); // 輸出: Task 1 或 Task 2(取決于哪個先完成)
任務編排異常處理
- 捕獲異常(
exceptionally),在任務拋出異常時提供默認信息。
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("失敗");
return "成功";
}).exceptionally(ex -> {
System.out.println("異常處理: " + ex.getMessage());
return "默認值";
});
System.out.println(future.get());
- 全局處理(
handle / whenComplete)
handle:處理異常并返回新結果
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("失敗");
return "成功";
}).handle((result, ex) -> {
if (ex != null) {
System.out.println("異常處理: " + ex.getMessage());
return "默認值";
}
return result;
});
whenComplete:無論成功或失敗均執行(不可中斷)
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (Math.random() > 0.5) throw new RuntimeException("失敗");
return "成功";
}).whenComplete((result, ex) -> {
if (ex != null) System.out.println("任務失敗");
else System.out.println("任務成功: " + result);
});
作者:紀莫
歡迎任何形式的轉載,但請務必注明出處。
限于本人水平,如果文章和代碼有表述不當之處,還請不吝賜教。
歡迎掃描二維碼關注公眾號:Jimoer
文章會同步到公眾號上面,大家一起成長,共同提升技術能力。
聲援博主:如果您覺得文章對您有幫助,可以點擊文章右下角【推薦】一下。
您的鼓勵是博主的最大動力!


線程同步的方式有哪些?什么情況下會產生死鎖?如何解決死鎖?死鎖如何恢復?數據庫操作中如何避免死鎖?CompletableFuture怎么實現多線程異步編排?
浙公網安備 33010602011771號