如何在Spring中使用注解解决线程并发问题?

在 Spring 中,线程并发问题的核心是共享资源的竞争(如多线程读写同一变量、并发操作数据库等),注解本身不能直接解决并发问题,但 Spring 提供了配套注解结合并发编程规范,可以优雅地实现线程安全控制。

下面我会从最常用的场景出发,讲解 Spring 中如何通过注解解决并发问题,包括方法级别的同步控制分布式锁(解决多实例并发)等核心方案。

一、基础场景:单实例下的方法同步(@Synchronized 或 @Lock)

Spring 本身没有内置的 @Synchronized 注解,但可以通过 AOP 自定义注解 实现方法级别的同步锁,替代原生 synchronized 关键字,让代码更优雅。

1. 自定义同步注解 + AOP 实现
步骤 1:定义同步注解
复制代码
import java.lang.annotation.*;

/**
 * 自定义同步注解,用于标记需要同步的方法
 */
@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MethodSynchronized {
    // 可指定锁的名称(可选)
    String lockKey() default "";
}
步骤 2:实现 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;

/**
 * 同步注解的切面实现
 */
@Aspect
@Component // 交给 Spring 管理
public class SynchronizedAspect {

    // 维护锁的缓存,避免重复创建锁
    private final ConcurrentHashMap<String, Lock> lockMap = new ConcurrentHashMap<>();

    /**
     * 环绕通知:拦截所有标注了 @MethodSynchronized 的方法
     */
    @Around("@annotation(methodSynchronized)")
    public Object around(ProceedingJoinPoint joinPoint, MethodSynchronized methodSynchronized) throws Throwable {
        // 1. 确定锁的 key(默认用方法全限定名,也可自定义)
        String lockKey = methodSynchronized.lockKey().isEmpty() 
                ? joinPoint.getSignature().toLongString() 
                : methodSynchronized.lockKey();
        
        // 2. 获取或创建锁(ConcurrentHashMap 保证线程安全)
        Lock lock = lockMap.computeIfAbsent(lockKey, k -> new ReentrantLock());
        
        try {
            lock.lock(); // 加锁
            return joinPoint.proceed(); // 执行原方法
        } finally {
            lock.unlock(); // 释放锁(finally 确保锁一定会释放)
        }
    }
}
步骤 3:使用注解解决并发问题
复制代码
import org.springframework.stereotype.Service;

@Service
public class OrderService {
    // 共享资源:模拟库存
    private int stock = 100;

    /**
     * 扣减库存(并发场景)
     * 使用 @MethodSynchronized 保证方法执行的原子性
     */
    @MethodSynchronized
    public void deductStock(int num) {
        if (stock >= num) {
            // 模拟耗时操作(如数据库查询)
            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:测试并发效果
复制代码
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 个线程并发扣减库存(每个扣减 10)
        for (int i = 0; i < 10; i++) {
            new Thread(() -> orderService.deductStock(10)).start();
        }
    }
}

输出结果(无并发问题,库存最终为 0):

复制代码
扣减成功,剩余库存:90
扣减成功,剩余库存:80
...
扣减成功,剩余库存:0
2. 进阶:按参数加锁(解决细粒度并发)

如果希望仅对相同参数的请求加锁(比如同一订单号的并发操作),可以修改 AOP 切面,将参数纳入锁的 key:

复制代码
// 修改 SynchronizedAspect 的 around 方法
String lockKey = methodSynchronized.lockKey().isEmpty() 
        ? joinPoint.getSignature().toLongString() + Arrays.toString(joinPoint.getArgs()) 
        : methodSynchronized.lockKey() + Arrays.toString(joinPoint.getArgs());

二、分布式场景:多实例并发(@RedissonLock)

如果你的应用部署在多台服务器(多实例),本地锁(如 ReentrantLocksynchronized)会失效,此时需要用分布式锁,Spring 中可结合 Redis(Redisson)实现注解式分布式锁。

1. 依赖引入(Maven)

xml

复制代码
<!-- 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 表达式,如 "#orderId")
    String key();
    // 锁的过期时间(防止死锁)
    long expireTime() default 30;
    // 时间单位
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}
3. AOP 实现分布式锁切面
复制代码
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 表达式解析器
    private final ExpressionParser parser = new SpelExpressionParser();
    // 参数名解析器(获取方法参数名)
    private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

    @Around("@annotation(redissonLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
        // 1. 解析 SpEL 表达式,获取锁的 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. 执行原方法
            return joinPoint.proceed();
        } finally {
            // 5. 释放锁(仅当前线程持有锁时释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    // 解析 SpEL 表达式
    private String parseSpEL(ProceedingJoinPoint joinPoint, String spEL) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 获取方法参数名
        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
        // 构建 SpEL 上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }
        // 解析表达式
        return parser.parseExpression(spEL).getValue(context, String.class);
    }
}
4. 使用分布式锁注解
复制代码
@Service
public class OrderService {
    /**
     * 提交订单(分布式并发场景)
     * @param orderId 订单号(按订单号加锁,不同订单不互斥)
     */
    @RedissonLock(key = "'order:' + #orderId") // SpEL 表达式,最终 key 为 "order:123456"
    public void submitOrder(String orderId) {
        // 并发操作:如扣减库存、生成订单
        System.out.println("订单 " + orderId + " 提交成功");
    }
}

三、其他补充方案

  1. Spring 事务注解(@Transactional) :虽然 @Transactional 主要用于事务管理,但它的隔离级别 (如 Isolation.REPEATABLE_READ)可以解决数据库层面的并发问题(如脏读、不可重复读)。例如:

    复制代码
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    public void updateUserBalance(Long userId, BigDecimal amount) {
        // 数据库并发更新操作
    }
  2. Spring 缓存注解(@Cacheable) :对于读多写少的场景,@Cacheable 可以将查询结果缓存到 Redis 等介质,减少数据库的并发访问,间接降低并发压力。

总结

  1. 单实例并发 :通过自定义 @MethodSynchronized 注解 + AOP 实现方法级同步锁,替代原生 synchronized,支持细粒度(按参数)加锁。
  2. 分布式并发 :结合 Redisson 实现 @RedissonLock 注解,通过 Redis 分布式锁解决多实例的并发问题,核心是 "锁的 key 唯一性 + 自动过期防死锁"。
  3. 数据库层并发 :使用 @Transactional 配置合适的隔离级别,解决数据库层面的并发问题;读多写少场景可结合 @Cacheable 减少并发压力。

核心原则:注解是 "语法糖",真正解决并发的是锁机制 (本地锁 / 分布式锁)和原子性保证,注解只是让锁的使用更优雅、更易维护。

相关推荐
future02102 小时前
Spring IOC启动全流程解密
java·后端·spring·ioc
太阳神LoveU2 小时前
Spring Boot 4.0.3和3.X的各个版本主要功能差别和优劣势对比
java·spring boot·后端
俩娃妈教编程2 小时前
C++基础知识点:位运算
java·开发语言·jvm·c++·位运算
zhoupenghui1682 小时前
golang 锁实现原理与解析&锁机制(sync)种类与举例说明以及其使用场景
开发语言·后端·golang·mutex·wait·lock·sync
掘金者阿豪2 小时前
从“多库掣肘”到“一库平川”:金仓KingbaseES的融合数据库深度体验
后端
夫唯不争,故无尤也2 小时前
原始文档元数据metadata
java·前端·javascript·sql
wefly20172 小时前
无需安装的 M3U8 在线播放器,快速实现 HLS 流预览与调试
java·开发语言·python·开发工具
Java编程爱好者2 小时前
面试被问 Redis?这 3 个问题 90% 的人都答不对
后端
金牌归来发现妻女流落街头2 小时前
【Spring AMQP 三大交换机】
后端·spring