1. Spring并发问题的本质与挑战
在企业级Java开发中,Spring框架的默认单例模式就像办公室里共享的打印机——所有人都用同一台设备,如果不加管控就会引发资源争抢。Spring容器中的Bean默认采用Singleton作用域,这意味着所有线程共享同一个Bean实例。当多个线程同时访问这个Bean的可变状态时,就会像多人同时编辑同一份文档却不加版本控制,必然导致数据混乱。
1.1 单例Bean的线程安全隐患
让我们解剖一个典型的反例。假设有个统计服务需要记录访问次数:
java复制@Component
public class VisitCounter {
private int count = 0; // 共享变量
public void addVisit() {
count++; // 非原子操作
}
public int getCount() {
return count;
}
}
这个简单的count++操作实际上包含三个步骤:读取当前值、值加1、写回新值。当两个线程同时执行时,可能出现以下时序:
- 线程A读取count=0
- 线程B读取count=0
- 线程A计算0+1=1
- 线程B计算0+1=1
- 线程A写入count=1
- 线程B写入count=1
尽管发生了两次调用,最终count却只增加了1。这就是典型的竞态条件(Race Condition)。
1.2 并发问题的三大类型
在Spring应用中,我们主要面临三类并发问题:
- 可见性问题:一个线程对共享变量的修改,另一个线程不能立即看到
- 原子性问题:看似不可分割的操作实际上由多个步骤组成
- 有序性问题:程序执行的顺序与代码顺序不一致
提示:现代CPU的多级缓存架构和指令重排序优化是这些问题产生的硬件根源。Spring并不直接解决这些问题,而是提供机制让我们更容易应用Java的并发解决方案。
2. Spring的并发安全工具箱
2.1 无状态设计模式
最彻底的解决方案是消除共享状态。这就像把共享打印机改为每人配备独立设备,自然就不会有冲突。在Spring中,我们可以这样改造计数器:
java复制@Component
public class StatelessCounter {
public int increment(int current) {
return current + 1; // 仅使用局部变量
}
}
这种设计下,调用者需要维护当前计数状态,服务本身不保存任何数据。Spring MVC中的Controller和Service层大多采用这种模式。
无状态设计的优势:
- 天然线程安全
- 易于测试和维护
- 适合分布式环境扩展
适用场景:
- 纯计算服务
- 数据转换服务
- 大部分业务逻辑层
2.2 作用域控制策略
当必须保持状态时,合理控制Bean的作用域就像为不同部门分配专用设备。Spring提供多种作用域:
| 作用域 | 说明 | 线程安全 | 典型应用场景 |
|---|---|---|---|
| singleton | 容器中唯一实例(默认) | 需保障 | 无状态服务 |
| prototype | 每次获取都创建新实例 | 安全 | 有状态对象工厂 |
| request | 每个HTTP请求创建新实例 | 安全 | 用户请求上下文 |
| session | 每个用户会话创建新实例 | 需注意 | 用户个性化设置 |
| application | ServletContext生命周期 | 需保障 | 全局共享资源 |
| websocket | WebSocket会话生命周期 | 安全 | 实时通信处理器 |
配置Request作用域的Bean示例:
java复制@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserPreference {
private String theme;
// getters/setters...
}
注意:对于非singleton作用域,proxyMode必须正确设置。TARGET_CLASS表示使用CGLIB代理,INTERFACES表示使用JDK动态代理。
2.3 Java并发工具集成
Spring无缝集成了Java并发包(java.util.concurrent)中的工具,就像为办公室配备了专业的调度系统。
2.3.1 原子变量
改造之前的计数器:
java复制@Component
public class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
}
原子类利用CPU的CAS(Compare-And-Swap)指令实现无锁线程安全,适合计数器、标志位等场景。
2.3.2 显式锁机制
对于复杂操作,可以使用ReentrantLock:
java复制@Component
public class OrderService {
private final Lock lock = new ReentrantLock();
public void processOrder(Order order) {
lock.lock();
try {
// 临界区代码
checkInventory();
deductStock();
createOrder();
} finally {
lock.unlock();
}
}
}
与synchronized相比,显式锁提供:
- 可中断的获取锁
- 超时获取锁
- 公平性选择
- 条件变量支持
2.3.3 并发集合
替换传统集合为线程安全版本:
java复制@Component
public class ProductCache {
private final ConcurrentMap<Long, Product> cache =
new ConcurrentHashMap<>();
public Product getProduct(Long id) {
return cache.computeIfAbsent(id,
key -> productDao.loadProduct(key));
}
}
常用并发集合:
- ConcurrentHashMap:分段锁实现的Map
- CopyOnWriteArrayList:写时复制List
- BlockingQueue:阻塞队列
- ConcurrentLinkedQueue:无界非阻塞队列
2.4 事务与并发控制
数据库操作中的并发问题需要特殊处理。Spring的事务管理就像给数据库操作加上了调度规则。
2.4.1 隔离级别配置
java复制@Service
public class BankService {
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transfer(Long from, Long to, BigDecimal amount) {
// 转账业务逻辑
}
}
Spring支持的标准隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能影响 |
|---|---|---|---|---|
| READ_UNCOMMITTED | 可能 | 可能 | 可能 | 最低 |
| READ_COMMITTED | 避免 | 可能 | 可能 | 低 |
| REPEATABLE_READ | 避免 | 避免 | 可能 | 中 |
| SERIALIZABLE | 避免 | 避免 | 避免 | 高 |
2.4.2 悲观锁实现
java复制@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.id = :id")
Product findByIdForUpdate(@Param("id") Long id);
}
悲观锁适合写多读少的场景,但会导致性能下降。
2.4.3 乐观锁实现
java复制@Entity
public class Product {
@Id
private Long id;
@Version
private Integer version;
// other fields...
}
@Service
public class ProductService {
@Transactional
public void updateProduct(Product product) {
productRepository.save(product);
// 如果version被其他事务修改过,会抛出OptimisticLockException
}
}
乐观锁适合读多写少的场景,通过版本号或时间戳实现。
2.5 AOP并发控制
Spring AOP可以统一处理并发控制逻辑,就像为方法调用安装智能门禁系统。
2.5.1 方法级同步
java复制@Aspect
@Component
public class SynchronizedAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object syncOperation(ProceedingJoinPoint pjp) throws Throwable {
synchronized(this) {
return pjp.proceed();
}
}
}
2.5.2 分布式锁实现
java复制@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {
private final RedissonClient redisson;
@Around("@annotation(lock)")
public Object withLock(ProceedingJoinPoint pjp, DistributedLock lock) throws Throwable {
String lockKey = evalSpEL(lock.key(), pjp);
RLock rLock = redisson.getLock(lockKey);
try {
if (!rLock.tryLock(lock.waitTime(), lock.leaseTime(), lock.unit())) {
throw new LockAcquisitionException("获取锁超时");
}
return pjp.proceed();
} finally {
rLock.unlock();
}
}
private String evalSpEL(String expr, ProceedingJoinPoint pjp) {
// 解析SpEL表达式
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key();
long waitTime() default 3;
long leaseTime() default 10;
TimeUnit unit() default TimeUnit.SECONDS;
}
3. 实战中的并发陷阱与解决方案
3.1 @Async的并发问题
异步方法虽然运行在不同线程,但仍可能共享资源:
java复制@Service
public class NotificationService {
private final List<String> logs = new ArrayList<>(); // 非线程安全
@Async
public void logNotification(String message) {
logs.add(message); // 并发问题!
}
}
解决方案:
- 使用CopyOnWriteArrayList
- 改为方法内部创建集合
- 使用ThreadLocal变量
3.2 缓存穿透问题
高并发下缓存失效可能导致数据库压力骤增:
java复制@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository repository;
private final CacheManager cacheManager;
@Cacheable("products")
public Product getProduct(Long id) {
return repository.findById(id).orElse(null);
}
}
当大量请求查询不存在的ID时,都会穿透到数据库。
解决方案:
- 缓存空值
- 使用Bloom过滤器
- 加互斥锁
优化后的实现:
java复制public Product getProduct(Long id) {
Product product = cacheManager.getCache("products").get(id, Product.class);
if (product != null) {
return product == NULL_PRODUCT ? null : product;
}
synchronized (this) {
product = cacheManager.getCache("products").get(id, Product.class);
if (product == null) {
product = repository.findById(id).orElse(NULL_PRODUCT);
cacheManager.getCache("products").put(id, product);
}
}
return product == NULL_PRODUCT ? null : product;
}
3.3 ThreadLocal的使用与清理
ThreadLocal可以保存线程私有数据,但在Web应用中需要特别注意:
java复制@Component
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public void setUser(User user) {
currentUser.set(user);
}
public User getUser() {
return currentUser.get();
}
public void clear() {
currentUser.remove();
}
}
@ControllerAdvice
public class UserContextCleaner implements HandlerInterceptor {
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
userContext.clear();
}
}
如果不及时清理,可能导致内存泄漏或信息泄露。
4. 性能优化与最佳实践
4.1 锁粒度控制
粗粒度锁虽然安全但影响性能:
java复制// 不推荐
public synchronized void processOrder(Order order) {
// 整个方法加锁
}
推荐细粒度锁:
java复制private final Map<Long, Lock> orderLocks = new ConcurrentHashMap<>();
public void processOrder(Order order) {
Lock lock = orderLocks.computeIfAbsent(order.getId(),
id -> new ReentrantLock());
lock.lock();
try {
// 只锁定特定订单
} finally {
lock.unlock();
}
}
4.2 读写锁应用
读多写少的场景适合ReadWriteLock:
java复制@Component
public class ProductInventory {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private Map<Long, Integer> inventory = new HashMap<>();
public int getInventory(Long productId) {
rwLock.readLock().lock();
try {
return inventory.getOrDefault(productId, 0);
} finally {
rwLock.readLock().unlock();
}
}
public void updateInventory(Long productId, int delta) {
rwLock.writeLock().lock();
try {
inventory.merge(productId, delta, Integer::sum);
} finally {
rwLock.writeLock().unlock();
}
}
}
4.3 并发测试策略
确保并发安全需要专门的测试:
java复制@Test
public void testConcurrentAccess() throws InterruptedException {
int threadCount = 100;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.execute(() -> {
try {
counter.increment();
} finally {
latch.countDown();
}
});
}
latch.await();
assertEquals(threadCount, counter.getCount());
}
使用AssertJ的并发断言:
java复制assertThat(counter).hasConcurrentValue(100);
5. Spring Reactor的响应式并发
现代Spring应用还可以采用响应式编程模型:
java复制@Service
public class ReactiveProductService {
private final ReactiveProductRepository repository;
public Mono<Product> getProduct(Long id) {
return repository.findById(id)
.timeout(Duration.ofSeconds(1))
.retryWhen(Retry.backoff(3, Duration.ofMillis(100)));
}
public Flux<Product> searchProducts(String keyword) {
return repository.findByNameContaining(keyword)
.subscribeOn(Schedulers.boundedElastic())
.publishOn(Schedulers.parallel());
}
}
响应式编程的特点:
- 非阻塞I/O
- 背压支持
- 声明式错误处理
- 灵活的调度控制
在实际项目中,我通常会根据业务特点选择并发策略。对于简单的CRUD服务,无状态设计配合数据库事务就足够;对于高并发秒杀系统,则需要组合使用缓存、分布式锁和限流措施。记住没有银弹,最合适的方案总是取决于具体场景。