Featured image of post Java 并发操作导致的问题

Java 并发操作导致的问题

在对方法加了锁的基础上,使用多线程操作数据库,发现仍然出现了多线程读取脏数据的情况,导致插入数据库的数据不正确

今天在本地模拟进行多线程的数据库访问操作,发现同时使用synchronized@Transactional的时候,线程的锁机制竟然没有生效,导致插入数据库中的数据依然是错误数据,为此进行深入分析理解这其中原理。

问题

Service层更新数据方法上添加了@Transactional注解作为数据库事务,便于后续插入异常回滚,同时又在该方法上添加了synchronized关键字,用于防止多线程条件下数据读取与插入出现脏数据的情况,然而执行测试方法时,发现synchronized竟然没有起到应有的锁的作用,主要代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Transactional(value = "master_tr")
@CachePut(key = "#amount.serialNo")
@TargetDataSource
@Override
public synchronized Amount updateAmount(Amount amount) {
    logger.info("begin to get ...");
    Amount oldAmount = amountDao.findAmountBySerialNo(amount.getSerialNo());
    logger.info(oldAmount.toString());
    amount.setMoney(amount.getMoney().add(oldAmount.getMoney()));
    logger.info("updateAmount, amount: {}, thread name: {}", amount.toString(), Thread.currentThread().getName());
    amountDao.updateAmount(amount);
    return amount;
}

通过查阅资料,发现synchronized@Transactional不能一起使用,在一起使用的情况下,会出现读写出脏数据的情况。

锁与事务

synchronized

synchronized作为解决并发问题的常用解决方法,一般有以下三种使用方式:

  • 用在实例方法上,锁住当前对象来同步方法
  • 用在静态方法上,锁的对象是当前Class
  • 用在同步块,锁的是{}中的对象

实现原理:
JVM利用进入、退出对象监视器(Monitor)来实现对方法、同步块同步的;从编译后的信息可以看出,在同步方法调用前添加了一个monitot.enter指令,同步方法调用结束处调用了monitor.exit指令,以此来实现一个排他锁,当某个线程获取到该锁的时候,就获取到了执行该方法的权限,而其它的线程则需要在该方法的入口处等待当前线程结束执行之后(即等到当前线程执行到monitor.exit处),再重新竞争获取锁。

我们通过一段代码来演示
源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package org.ws.tanyunshou.synch;

/**
 * @author yinan
 * @date 19-1-1
 */
public class Synchronized {

    public static void main(String[] args) {
        synchronized (Synchronized.class) {
            System.out.println("Synchronized");
        }
    }

}

通过javap -c Synchronized.class查看编译之后的信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Compiled from "Synchronized.java"
public class org.ws.tanyunshou.synch.Synchronized {
  public org.ws.tanyunshou.synch.Synchronized();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class org/ws/tanyunshou/synch/Synchronized
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Synchronized
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}

通过查看编译后的信息,我们可以看到monitorentermonitorexit两个指令,这两个指令也便是指定了线程获取和释放锁的时机。

@Transactional

@Transactional是通过Spring AOP的方式开启事务的,因此会在updateAmount方法前开启事务,之后再加锁,当锁住的代码完成后,再提交事务。因此,synchronized代码块执行的代码是在事务之内执行的,所以可推断在代码执行完成时,事务还没有提交,因此其它线程进入synchronized代码块中,读取数据库的数据并不是最新的。

解决方法:
将以上一个方法拆分成两个方法,通过synchronized方法调用updateAmount方法,以此实现在还没有开启事务之前就加锁,那么便可以保证线程的同步。目前本人实在同一个类中将原本的一个方法拆分成了两个方法来执行,具体代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/***
     * 更新数据
     * @CachePut 保存进redis
     * @TragetDataSource 默认的数据源
     * @param amount
     * @return
     */
    @CachePut(key = "#amount.serialNo")
    @TargetDataSource
    @Override
    public Amount updateAmount(Amount amount) {
        readWriteLock.writeLock().lock();
        Amount oldAmount = amountDao.findAmountBySerialNo(amount.getSerialNo());
        amount.setMoney(amount.getMoney().add(oldAmount.getMoney()));
        logger.info("updateAmount, amount: {}, thread name: {}", amount.toString(), Thread.currentThread().getName());
        rellayUpdateAmount(amount);
        readWriteLock.writeLock().unlock();
        return amount;
    }

    /**
     * 独立事务
     * 解决事务与锁机制无法一起使用的问题
     * @param amount
     * @return
     */
    @Transactional(value = "master_tr", rollbackFor = SQLException.class)
    @TargetDataSource
    public void rellayUpdateAmount(Amount amount) {
        amountDao.updateAmount(amount);
    }

以上案例自己测试没有出现问题,但是也有网友指出,“在同一个实例中调用被@Transactional注解的另一个方法,且两个方法是属于同一个类时,事务不会生效”,因此推荐将这两个方法分别写在不同类中,再去实现调用关系,以避免出现事务失败的问题。

本文参考

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus