learning_record_doc/java/踩坑记录/Synchronized 锁在 Spring 事务管理下,为啥还线程不安全.md
2022-12-31 17:33:47 +08:00

6.8 KiB
Raw Permalink Blame History

本文由 简悦 SimpRead 转码, 原文地址 www.likecs.com

开启 10000 个线程,每个线程给员工表的 money 字段【初始值是 0】加 1没有使用悲观锁和乐观锁但是在业务层方法上加了 synchronized 关键字,问题是代码执行完毕后数据库中的 money 字段不是......

开启 10000 个线程,每个线程给员工表的 money 字段【初始值是 0】加 1没有使用悲观锁和乐观锁但是在业务层方法上加了 synchronized 关键字,问题是代码执行完毕后数据库中的 money 字段不是 10000而是小于 10000 问题出在哪里?

Service 层代码:

SQL 代码 (没有加悲观 / 乐观锁)

用 1000 个线程跑代码:

简单来说:多线程跑一个使用 synchronized 关键字修饰的方法,方法内操作的是数据库,按正常逻辑应该最终的值是 1000但经过多次测试结果是低于 1000。这是为什么呢

既然测试出来的结果是低于 1000那说明这段代码不是线程安全的。不是线程安全的那问题出现在哪呢众所周知synchronized 方法能够保证所修饰的代码块、方法保证有序性、原子性、可见性。

讲道理,以上的代码跑起来,问题中 Service 层的 increaseMoney() 是有序的、原子的、可见的,所以断定跟 synchronized 应该没关系。

(参考我之前写过的 synchronize 锁笔记Java 锁机制了解一下)

既然 Java 层面上找不到原因,那分析一下数据库层面的吧 (因为方法内操作的是数据库)。在 increaseMoney() 方法前加了 @Transcational 注解,说明这个方法是带有事务的。事务能保证同组的 SQL 要么同时成功,要么同时失败。讲道理,如果没有报错的话,应该每个线程都对 money 值进行 + 1。从理论上来说结果应该是 1000 的才对。

(参考我之前写过的 Spring 事务:一文带你看懂 Spring 事务!)

根据上面的分析,我怀疑是提问者没测试好 (hhhh逃),于是我也跑去测试了一下,发现是以提问者的方式来使用是真的有问题

首先贴一下我的测试代码:

@RestController
public class EmployeeController {
 @Autowired
 private EmployeeService employeeService;
 @RequestMapping("/add")
 public void addEmployee() {
 for (int i = 0; i < 1000; i++) {
 new Thread(() -> employeeService.addEmployee()).start();
 }
 }
}
@Service
public class EmployeeService {
 @Autowired
 private EmployeeRepository employeeRepository;
 @Transactional
 public synchronized void addEmployee() {
 // 查出ID为8的记录然后每次将年龄增加一
 Employee employee = employeeRepository.getOne(8);
 System.out.println(employee);
 Integer age = employee.getAge();
 employee.setAge(age + 1);
 employeeRepository.save(employee);
 }
}


简单地打印了每次拿到的 employee 值,并且拿到了 SQL 执行的顺序,如下 (贴出小部分)

从打印的情况我们可以得出:多线程情况下并没有串行执行 addEmployee() 方法。这就导致对同一个值做重复的修改,所以最终的数值比 1000 要少。

发现并不是同步执行的,于是我就怀疑 synchronized 关键字和 Spring 肯定有点冲突。于是根据这两个关键字搜了一下,找到了问题所在。

我们知道 Spring 事务的底层是 Spring AOP而 Spring AOP 的底层是动态代理技术。跟大家一起回顾一下动态代理:

 public static void main(String[] args) {
 // 目标对象
 Object target ;
 Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
 @Override
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 // 但凡带有@Transcational注解的方法都会被拦截
 // 1... 开启事务
 method.invoke(target);
 // 2... 提交事务
 return null;
 }
 
 });
 }


(详细请参考我之前写过的动态代理:给女朋友讲解什么是代理模式)

实际上 Spring 做的处理跟以上的思路是一样的,我们可以看一下 TransactionAspectSupport 类中 invokeWithinTransaction()

调用方法开启事务,调用方法提交事务

在多线程环境下,就可能会出现:方法执行完了 (synchronized 代码块执行完了),事务还没提交,别的线程可以进入被 synchronized 修饰的方法,再读取的时候,读到的是还没提交事务的数据,这个数据不是最新的,所以就出现了这个问题。

从上面我们可以发现,问题所在是因为 @Transcational 注解和 synchronized 一起使用了,加锁的范围没有包括到整个事务。所以我们可以这样做:

新建一个名叫 SynchronizedService 类,让其去调用 addEmployee() 方法,整个代码如下:

@RestController
public class EmployeeController {
 @Autowired
 private SynchronizedService synchronizedService ;
 @RequestMapping("/add")
 public void addEmployee() {
 for (int i = 0; i < 1000; i++) {
 new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();
 }
 }
}
// 新建的Service类
@Service
public class SynchronizedService {
 @Autowired
 private EmployeeService employeeService ;
	
 // 同步
 public synchronized void synchronizedAddEmployee() {
 employeeService.addEmployee();
 }
}
@Service
public class EmployeeService {
 @Autowired
 private EmployeeRepository employeeRepository;
 
 @Transactional
 public void addEmployee() {
 // 查出ID为8的记录然后每次将年龄增加一
 Employee employee = employeeRepository.getOne(8);
 System.out.println(Thread.currentThread().getName() + employee);
 Integer age = employee.getAge();
 employee.setAge(age + 1);
 employeeRepository.save(employee);
 }
}


我们将 synchronized 锁的范围包含到整个 Spring 事务上,这就不会出现线程安全的问题了。在测试的时候,我们可以发现 1000 个线程跑起来比之前要慢得多,当然我们的数据是正确的:

可以发现的是,虽然说 Spring 事务用起来我们是非常方便的,但如果不了解一些 Spring 事务的细节,很多时候出现 Bug 了就百思不得其解。还是得继续加油努力呀~~~

笔者注:这个问题的核心是 synchronized 方法虽然能实现方法的同步,但是却未能实现数据库操作的同步,因为 synchronized 推出时事务还没有提交,这个时候有可能有其他线程进入该方法,如果在上一事务未提交前就读取数据,那么此时读取的数据就会有误!!!