序
今天在本地模拟进行多线程的数据库访问操作,发现同时使用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
}
|
通过查看编译后的信息,我们可以看到monitorenter
和monitorexit
两个指令,这两个指令也便是指定了线程获取和释放锁的时机。
@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
注解的另一个方法,且两个方法是属于同一个类时,事务不会生效”,因此推荐将这两个方法分别写在不同类中,再去实现调用关系,以避免出现事务失败的问题。
本文参考