在多线程环境操作事务时,synchronized使用不当引发的线程安全问题,通过现象来了解Spring Aop原理

我们都知道,在多线程环境,可以用synchronized来做多线程同步,保护临界区资源,达到线程安全的目的。我们也知道synchronized和ReentrantLock的区别,如果不清楚两者区别的,请参考《synchronized和ReentrantLock的区别与适用场景的解析》。假设读这篇文章前大家已经会使用Springboot+Mysql+Mybatis,首先在文章前面提出几个问题,看你是否都知道,文章中会通过具体的代码来验证这些问题:

  1. 如下代码,没有加任何锁机制,多线程环境调用该方法,是否引发线程安全问题。例如,假定studentId=1的age初始值为0,50个线程同时调用该方法,age最终是多少
	@Transactional(rollbackFor = Exception.class)
	public void incrementAge(Long studentId) {
	     studentMapper.incrementAge(studentId);
	}
	<update id="incrementAge">
	        update demo_student
	        set age=age+1
	        where id=#{studentId}
	</update>
  1. 如下代码,synchronized关键字加在事务方法上,多线程环境调用该事务方法,是否能保证线程安全,例如,假定studentId=1的age初始值为0,50个线程同时调用该方法,age最终是多少
	@Transactional(rollbackFor = Exception.class)
	public synchronized void syncIncrement(Long studentId) {
	    Student student = studentMapper.selectOne(studentId);
	    student.setAge(student.getAge() + 1);
	    studentMapper.updateStudent(student);
	}
	 <update id="updateStudent">
	        update demo_student
	        set age=#{student.age}
	        where id=#{student.id}
	 </update>
  1. 如下代码,事务方法加上static,数据库操作以后抛出异常,事务是否会回滚。例如,假定studentId=1的age初始值为0,调用一次该方法,age是0还是1
	@Transactional(rollbackFor = Exception.class)
    public static  void staticIncrement(Long studentId) {
        try {
            StudentMapper studentMapper = SpringUtils.getBean(StudentMapper.class);
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            studentMapper.updateStudent(student);

            System.out.println(1/0);
        }catch (Exception e){
            e.printStackTrace();
        }finally {

        }
    }
  1. 这算是额外的问题,与本篇文章话题无关,但会考住很多人,下面这段代码finally块抛出异常,事务是否回滚,这里不做解答,不知道的可以验证一下
	@Transactional(rollbackFor = Exception.class)
    public int increment(Long studentId) {
        try {
            StudentMapper studentMapper = SpringUtils.getBean(StudentMapper.class);
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            return studentMapper.updateStudent(student);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(1 / 0);
        }
        return 0;
    }
  1. 该问题也与本篇文章无直接关系,也顺便提一下,也会考住很多人。下面这段代码执行返回值是多少
	@Transactional(rollbackFor = Exception.class)
    public int increment(Long studentId) {
        try {
            StudentMapper studentMapper = SpringUtils.getBean(StudentMapper.class);
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            return studentMapper.updateStudent(student);

        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        } finally {
            return 100;
        }
    }

下面首先贴出本篇文章重要代码,后面解析和验证问题都需要用到

  • 数据库表:学生信息表,包含id、name、age三个字段,表中有一条数据,如下图
    在这里插入图片描述
  • mybatis代码:两种更新操作,一种自增,一种直接更新成具体的值
	<update id="incrementAge">
        update demo_student
        set age=age+1
        where id=#{studentId}
    </update>
    <update id="updateStudent">
        update demo_student
        set age=#{student.age}
        where id=#{student.id}
    </update>
  • mapper接口:
@Mapper
public interface StudentMapper {
    void incrementAge(@Param("studentId") Long studentId);

    int updateStudent(@Param("student") Student student);

    Student selectOne(@Param("studentId") Long studentId);
}
  • service代码:
	@Transactional(rollbackFor = Exception.class)
    public void incrementAge(Long studentId) {
        studentMapper.incrementAge(studentId);
    }

    @Transactional(rollbackFor = Exception.class)
    public synchronized int syncIncrement(Long studentId) {
        try {
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            return studentMapper.updateStudent(student);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        } finally {
        }
    }
    @Transactional(rollbackFor = Exception.class)
    public static int staticIncrement(Long studentId) {
        try {
            StudentMapper studentMapper=SpringUtils.getBean(StudentMapper.class);
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            return studentMapper.updateStudent(student);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        } finally {
        }
    }
    @Transactional(rollbackFor = Exception.class)
    public int increment(Long studentId) {
        try {
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            return studentMapper.updateStudent(student);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        } finally {
        }
    }

sql的原子性

  • 执行如下测试代码
    @Test
    void testIncrement() {
        CountDownLatch countDownLatch = new CountDownLatch(500);
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 500; i++) {
            executorService.execute(() -> {
                try {
                    userService.incrementAge(1L);
                    countDownLatch.countDown();
                } catch (Exception e) {
					e.printStackTrace();
                } finally {
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
  • 查看数据库age字段的值
    在这里插入图片描述
    从代码角度看,我们构造了一个固定大小的线程池,线程池核心线程50个,总共500个线程,也就是可能同时50个线程去并发的执行age的累加操作,我们并没有加任何的锁也没有加synchronized同步锁,但是最终数据保持准确性,可能有些人会有疑问,这就是sql的原子性问题。但这里如果换成调用increment方法(即先查询,再+1,最后更新到数据库),就会导致数据不准确,就是多线程环境未做临界资源的同步的结果。

结论:一条sql 具有原子性,不会有线程安全问题,和事务隔离级别没关系,因此问题1的答案就出来了。

synchronized失效

我们知道,synchronized如果加在普通方法上,锁住的就是该类的一个实例对象;synchronized如果加在静态方法上,锁住的就是该类;synchronized如果同步代码块,锁住的就是synchronized(obj)中的obj对象,obj可能是一个对象,或者类,或者当前实例对象。看下面测试代码:

	@Test
    void testIncrement() {
        CountDownLatch countDownLatch = new CountDownLatch(500);
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        for (int i = 0; i < 500; i++) {
            executorService.execute(() -> {
                try {
                    userService.syncIncrement(1L);
                    countDownLatch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

测试结果:
在这里插入图片描述
为什么我们在事务方法上加了synchronized同步锁,还会导致线程安全问题,这就跟synchronized锁对象和事务隔离级别有关。
比如在这里,userService.syncIncrement(1L),方法上的synchronized锁就是userService对象,spring aop的原理是JDK动态代理或CGLIB动态代理,在代理对象执行方法之前开启事务,在代理对象执行方法之后提交事务。

开启事务
代理对象执行方法 即userService.syncIncrement
提交事务

因此如果发生高并发,这个同步标志是失效的,原因就是事务在执行的时候,是由spring 生成的代理类执行,在代理方法执行前后锁住的不是同一个对象,如果此时有高并发,请求依然会进入到这个同步的方法内部,造成的结果就是数据库幻读,由于A线程事务还没有提交,B线程读取到的数据不正确造成最终数据不准确。
解决这种现象的方式有很多,这里列举几种:

  1. 加大synchronized同步范围,将synchronized加载调用事务方法外层,这样就可以保证synchronized的锁始终是同一个对象,临界区始终只会有一个线程进入。 如:
 			executorService.execute(() -> {
                try {
                    synchronized (lock) {
                        userService.syncIncrement(1L);
                        countDownLatch.countDown();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                }
    		});
  1. 方法同1类似,使用其他的锁,比如可重入锁ReentrantLock,效果和方案1相同,但在低版本的jdk性能比synchronized要好,低版本的synchronized是重量级锁
            executorService.execute(() -> {
                try {
                    reentrantLock.lock();
                    userService.syncIncrement(1L);
                    countDownLatch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    reentrantLock.unlock();
                }
            });
  1. 改变事务隔离级别,mysql四种事务隔离级别中,只有串行化可以防止幻读。串行化隔离级别规定,读锁未释放,写锁不可执行。读锁的获取并不是加了isolation = Isolation.SERIALIZABLE一进入syncIncrement方法就获得了读锁,而是在studentMapper.selectOne才获得了读锁,这里只要把读和写作为临界资源即可保证线程安全。
    因此下面显示指定事务隔离级别也可以解决线程安全问题,需配合synchronized,下面两种情况都是OK的。
	@Transactional(rollbackFor = Exception.class,isolation = Isolation.SERIALIZABLE)
    public synchronized int syncIncrement(Long studentId) {
        try {
            Student student = studentMapper.selectOne(studentId);
            student.setAge(student.getAge() + 1);
            return studentMapper.updateStudent(student);
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        } finally {
        }
    }
    
    @Transactional(rollbackFor = Exception.class)
    public int syncIncrement(Long studentId) {
        try {
            synchronized (this) {
                Student student = studentMapper.selectOne(studentId);
                student.setAge(student.getAge() + 1);
                return studentMapper.updateStudent(student);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return -1;
        } finally {
        }
    }

spring默认事务隔离级别与数据库底层保持一致,mysql中默认事务隔离级别为repeatable-read

/**
* Use the default isolation level of the underlying datastore.
* All other levels correspond to the JDBC isolation levels.
* @see java.sql.Connection
*/
int ISOLATION_DEFAULT = -1;

其他方案可以在评论区交流,笔者暂时想不到

静态方法注解事务失效

我们都知道spring 注解事务底层原理都是spring aop,而spring aop无非就是JDK动态代理和CGLIB动态代理,默认为JDK动态代理,通过@EnableTransactionManagement属性proxyTargetClass=true可以启动CGLIB代理,两种代理的介绍可以参照这篇文章《spring的动态代理模式有几种?默认是那种?如何切换?》

总结事务失效的情况,下面情况事务均会失效

1.静态方法

@Transactional(rollbackFor = Exception.class)
   public static int incrementThrow(Long studentId) {
       try {
          Student student = studentMapper.selectOne(studentId);
         student.setAge(student.getAge() + 1);
           return studentMapper.updateStudent(student);
       } catch (Exception e) {
           e.printStackTrace();
           return -1;
       } finally {
           System.out.println(1/0);
       }
   }

2.private或者protected方法

@Transactional(rollbackFor = Exception.class)
private int incrementThrow(Long studentId) {...}

@Transactional(rollbackFor = Exception.class)
protected int incrementThrow(Long studentId) {...}

3.自身非事务方法调用自身的事务方法,如通过调用invokeSelf间接调用事务方法

@Transactional(rollbackFor = Exception.class)
public int incrementThrow(Long studentId) {...}

public int invokeSelf(Long studentId){
     return this.incrementThrow(studentId);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽轩GM

您的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值