Spring使用注解解決線程并發(fā)問題的多種方案
引言
在 Spring 中,線程并發(fā)問題的核心是共享資源的競爭(如多線程讀寫同一變量、并發(fā)操作數(shù)據(jù)庫等),注解本身不能直接解決并發(fā)問題,但 Spring 提供了配套注解結(jié)合并發(fā)編程規(guī)范,可以優(yōu)雅地實現(xiàn)線程安全控制。
下面我會從最常用的場景出發(fā),講解 Spring 中如何通過注解解決并發(fā)問題,包括方法級別的同步控制、分布式鎖(解決多實例并發(fā))等核心方案。
一、基礎(chǔ)場景:單實例下的方法同步(@Synchronized 或 @Lock)
Spring 本身沒有內(nèi)置的 @Synchronized 注解,但可以通過 AOP 自定義注解 實現(xiàn)方法級別的同步鎖,替代原生 synchronized 關(guān)鍵字,讓代碼更優(yōu)雅。
1. 自定義同步注解 + AOP 實現(xiàn)
步驟 1:定義同步注解
import java.lang.annotation.*;
/**
* 自定義同步注解,用于標(biāo)記需要同步的方法
*/
@Target(ElementType.METHOD) // 僅作用于方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MethodSynchronized {
// 可指定鎖的名稱(可選)
String lockKey() default "";
}步驟 2:實現(xiàn) AOP 切面(核心)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 同步注解的切面實現(xiàn)
*/
@Aspect
@Component // 交給 Spring 管理
public class SynchronizedAspect {
// 維護(hù)鎖的緩存,避免重復(fù)創(chuàng)建鎖
private final ConcurrentHashMap<String, Lock> lockMap = new ConcurrentHashMap<>();
/**
* 環(huán)繞通知:攔截所有標(biāo)注了 @MethodSynchronized 的方法
*/
@Around("@annotation(methodSynchronized)")
public Object around(ProceedingJoinPoint joinPoint, MethodSynchronized methodSynchronized) throws Throwable {
// 1. 確定鎖的 key(默認(rèn)用方法全限定名,也可自定義)
String lockKey = methodSynchronized.lockKey().isEmpty()
? joinPoint.getSignature().toLongString()
: methodSynchronized.lockKey();
// 2. 獲取或創(chuàng)建鎖(ConcurrentHashMap 保證線程安全)
Lock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
try {
lock.lock(); // 加鎖
return joinPoint.proceed(); // 執(zhí)行原方法
} finally {
lock.unlock(); // 釋放鎖(finally 確保鎖一定會釋放)
}
}
}步驟 3:使用注解解決并發(fā)問題
import org.springframework.stereotype.Service;
@Service
public class OrderService {
// 共享資源:模擬庫存
private int stock = 100;
/**
* 扣減庫存(并發(fā)場景)
* 使用 @MethodSynchronized 保證方法執(zhí)行的原子性
*/
@MethodSynchronized
public void deductStock(int num) {
if (stock >= num) {
// 模擬耗時操作(如數(shù)據(jù)庫查詢)
try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
stock -= num;
System.out.println("扣減成功,剩余庫存:" + stock);
} else {
System.out.println("庫存不足");
}
}
// 獲取庫存(測試用)
public int getStock() {
return stock;
}
}步驟 4:測試并發(fā)效果
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TestConcurrent {
public static void main(String[] args) {
// 初始化 Spring 容器
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext("com.example");
OrderService orderService = context.getBean(OrderService.class);
// 模擬 10 個線程并發(fā)扣減庫存(每個扣減 10)
for (int i = 0; i < 10; i++) {
new Thread(() -> orderService.deductStock(10)).start();
}
}
}輸出結(jié)果(無并發(fā)問題,庫存最終為 0):
扣減成功,剩余庫存:90 扣減成功,剩余庫存:80 ... 扣減成功,剩余庫存:0
2. 進(jìn)階:按參數(shù)加鎖(解決細(xì)粒度并發(fā))
如果希望僅對相同參數(shù)的請求加鎖(比如同一訂單號的并發(fā)操作),可以修改 AOP 切面,將參數(shù)納入鎖的 key:
// 修改 SynchronizedAspect 的 around 方法
String lockKey = methodSynchronized.lockKey().isEmpty()
? joinPoint.getSignature().toLongString() + Arrays.toString(joinPoint.getArgs())
: methodSynchronized.lockKey() + Arrays.toString(joinPoint.getArgs());二、分布式場景:多實例并發(fā)(@RedissonLock)
如果你的應(yīng)用部署在多臺服務(wù)器(多實例),本地鎖(如 ReentrantLock、synchronized)會失效,此時需要用分布式鎖,Spring 中可結(jié)合 Redis(Redisson)實現(xiàn)注解式分布式鎖。
1. 依賴引入(Maven)
<!-- Redisson 分布式鎖核心依賴 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>2. 自定義分布式鎖注解
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissonLock {
// 鎖的 key(支持 SpEL 表達(dá)式,如 "#orderId")
String key();
// 鎖的過期時間(防止死鎖)
long expireTime() default 30;
// 時間單位
TimeUnit timeUnit() default TimeUnit.SECONDS;
}3. AOP 實現(xiàn)分布式鎖切面
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class RedissonLockAspect {
@Autowired
private RedissonClient redissonClient;
// SpEL 表達(dá)式解析器
private final ExpressionParser parser = new SpelExpressionParser();
// 參數(shù)名解析器(獲取方法參數(shù)名)
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(redissonLock)")
public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
// 1. 解析 SpEL 表達(dá)式,獲取鎖的 key
String key = parseSpEL(joinPoint, redissonLock.key());
// 2. 獲取分布式鎖
RLock lock = redissonClient.getLock(key);
try {
// 3. 加鎖(支持過期時間,防止死鎖)
boolean locked = lock.tryLock(0, redissonLock.expireTime(), redissonLock.timeUnit());
if (!locked) {
throw new RuntimeException("獲取分布式鎖失敗,請稍后重試");
}
// 4. 執(zhí)行原方法
return joinPoint.proceed();
} finally {
// 5. 釋放鎖(僅當(dāng)前線程持有鎖時釋放)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 解析 SpEL 表達(dá)式
private String parseSpEL(ProceedingJoinPoint joinPoint, String spEL) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 獲取方法參數(shù)名
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
// 構(gòu)建 SpEL 上下文
StandardEvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
// 解析表達(dá)式
return parser.parseExpression(spEL).getValue(context, String.class);
}
}4. 使用分布式鎖注解
@Service
public class OrderService {
/**
* 提交訂單(分布式并發(fā)場景)
* @param orderId 訂單號(按訂單號加鎖,不同訂單不互斥)
*/
@RedissonLock(key = "'order:' + #orderId") // SpEL 表達(dá)式,最終 key 為 "order:123456"
public void submitOrder(String orderId) {
// 并發(fā)操作:如扣減庫存、生成訂單
System.out.println("訂單 " + orderId + " 提交成功");
}
}三、其他補(bǔ)充方案
Spring 事務(wù)注解(@Transactional):雖然 @Transactional 主要用于事務(wù)管理,但它的隔離級別(如 Isolation.REPEATABLE_READ)可以解決數(shù)據(jù)庫層面的并發(fā)問題(如臟讀、不可重復(fù)讀)。例如:
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void updateUserBalance(Long userId, BigDecimal amount) {
// 數(shù)據(jù)庫并發(fā)更新操作
}Spring 緩存注解(@Cacheable):對于讀多寫少的場景,@Cacheable 可以將查詢結(jié)果緩存到 Redis 等介質(zhì),減少數(shù)據(jù)庫的并發(fā)訪問,間接降低并發(fā)壓力。
總結(jié)
- 單實例并發(fā):通過自定義 @MethodSynchronized 注解 + AOP 實現(xiàn)方法級同步鎖,替代原生
synchronized,支持細(xì)粒度(按參數(shù))加鎖。 - 分布式并發(fā):結(jié)合 Redisson 實現(xiàn) @RedissonLock 注解,通過 Redis 分布式鎖解決多實例的并發(fā)問題,核心是 “鎖的 key 唯一性 + 自動過期防死鎖”。
- 數(shù)據(jù)庫層并發(fā):使用
@Transactional配置合適的隔離級別,解決數(shù)據(jù)庫層面的并發(fā)問題;讀多寫少場景可結(jié)合@Cacheable減少并發(fā)壓力。
核心原則:注解是 “語法糖”,真正解決并發(fā)的是鎖機(jī)制(本地鎖 / 分布式鎖)和原子性保證,注解只是讓鎖的使用更優(yōu)雅、更易維護(hù)。
以上就是Spring使用注解解決線程并發(fā)問題的多種方案的詳細(xì)內(nèi)容,更多關(guān)于Spring注解解決線程并發(fā)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MyBatis中的collection兩種使用方法及效率比較
collection主要是應(yīng)對表關(guān)系是一對多的情況,本文主要介紹了MyBatis中的collection兩種使用方法及效率比較,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Java Lambda表達(dá)式和函數(shù)式接口實例分析
這篇文章主要介紹了Java Lambda表達(dá)式和函數(shù)式接口,結(jié)合實例形式分析了Java8 Lambda表達(dá)式和函數(shù)式接口相關(guān)原理、用法及操作注意事項,需要的朋友可以參考下2019-09-09
Springboot服務(wù)引用Nacos中新增的配置文件失敗問題及解決
這篇文章主要介紹了Springboot服務(wù)引用Nacos中新增的配置文件失敗問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-06-06
Java實現(xiàn)獲取銀行卡所屬銀行,驗證銀行卡號是否正確的方法詳解
這篇文章主要介紹了Java實現(xiàn)獲取銀行卡所屬銀行,驗證銀行卡號是否正確的方法,結(jié)合實例形式詳細(xì)分析了java判斷銀行卡歸屬地及有效性的原理與相關(guān)實現(xiàn)技巧,需要的朋友可以參考下2019-09-09

