在使用@Async注解时,假如出现线程冲突问题时怎么办呢?
比如下面的场景,在添加异步注解后可能会出现的问题
假设同时开启两个线程A和B,读的数在数据库里一开始为1,每次读后应当+1,那么两个线程读后应当最后会变为3
A(取1)
B(取1)
A(根据1改2)
B(根据1改2)
但是由于是异步处理,会出现上面的结果,最终会变为2。为此引入解决的办法,添加一个异步锁Lock
service层写法:
public interface ITestService {
@Async
void test(String schoolId)
}
serviceImpl层写法
@Service
@Slf4j
public class TestServiceImpl implements ITestService {
private ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void test(String schoolId) throws IllegalAccessException {
// 获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态
reentrantLock.lock();
try{
...
} catch (Exception e){
throw new ServiceException("业务异常");
}finally {
// 释放锁
reentrantLock.unlock();
}
}
这样改造的结果就变成了
A(取1)
A(根据1改2)
B(取2)
B(根据2改3)
此处理方法仅适用于部署了单个服务的场景。如果是分布式服务那要另外考虑引入redis分布式。
进阶修改
假如现在一个业务,要异步方法里面刷新某个表的记录,那么一条记录应当被上锁,
而不是整个方法都排队。
假如A线程和B线程同时改表X的x1记录,那么A和B应当排队,而另一条记录x2则是空闲的不需要排队的。
那么可以这样实现,加多一个map管理
@Service
@Slf4j
public class TestServiceImpl implements ITestService {
private Map<String,ReentrantLock> lockMap = new HashMap<>();
@Override
public void test(String schoolId) throws IllegalAccessException {
// 获取锁,如果锁不可用,则出于线程调度的目的,当前线程将被禁用,并且在获取锁之前处于休眠状态
// 根据指定的key获取指定的锁,没有就新建一个
if( !this.lockMap.containsKey(schoolId) ){
this.lockMap.put(schoolId,new ReentrantLock());
}
// 注意这里不能获取为局部变量,否则地址则不是原来的map里的value地址
this.lockMap.get(schoolId).lock();
try{
...
} catch (Exception e){
throw new ServiceException("业务异常");
}finally {
// 释放锁
this.lockMap.get(schoolId).unlock();
// 如果没有等待的线程队列则移除此key
if( !this.lockMap.get(schoolId).hasQueuedThreads() ){
lockMap.remove(schoolId);
}
}
}
为什么不使用悲观锁?
如果加个字段,如version,则每次更新数据要version相等才可更新。但是如果引入则会出现另一个问题,
假如你的事务是不可重复读的(事务B在重复读取中,读到的数据不一样,是因为A在B读取的期间改掉了数据)
假设version一开始为1,每次更新后会+1
A(取1)
B(取1)
A(根据1改2)
B(无法修改数据,因为此时version为2)
那样数据会是2,我们需要的数据应当是3,这样就造成数据的遗失了