synchronized没能锁住方法???学到了新知识

昨天,同事告诉我生产系统上竟然出现了两个一样的申请号,我表示十分震惊,申请号是系统中唯一标示(不是表主键),怎么会重复呢???

我去找了一下生成申请号的代码,第一眼就觉得没问题,明明是加了synchronized的方法,怎么会生成一样的申请号呢,我甚至开始怀疑synchronized到底是不是真的能保证线程同步了。并且我们也不是集群,就一个tomcat,完全没道理。

我看到数据库里这两条申请的创建时间是一模一样的,那还真是无巧不成书,也就是说这两个也业务员正好在同一时间点击了按钮。

然后找了两个同事,他们在测试环境模拟试试,然后一起喊着“3 2 1 点”,就复现了这个bug。(尴尬...)之前没出现这个bug是因为没人会闲着没事一起同时点按钮。

再去找找资料吧:

https://blog.csdn.net/qq_45173404/article/details/106616406

https://www.jb51.net/article/74566.htm

当synchronized用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前.这时,线程获得的是成员锁,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

看着没问题,贴一下我们生成申请号的逻辑和代码:

申请类型固定值开头+年月日+从1开始按天自增的3位序列号,如:TEST20210401001 ;

@Service("appNoImpl")
public class AppNoImpl implements IAppNo {

    @Resource
	private AppNoMapper appNoMapper;


    public synchronized String getOneAppNo(String appType) throws Exception {
		AppNo appNo = appNoMapper.selectByPrimaryKey(this.newSqlParam("ruleKey", appType));
		SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
		if(appNo == null){
			AppNo no = new AppNo();
			no.setMaxNo(2);
			String date = sdf.format(new Date());
			no.setRuleDate(date);
			no.setRuleKey(appType);
			appNoMapper.insert(this.newSqlParam("appNo", no));
			return appType+date+String.format("%03d", 1);
		}else{
			if(sdf.format(new Date()).equals(appNo.getRuleDate())){
				String appNum = appNo.getRuleKey()+appNo.getRuleDate()+String.format("%03d", appNo.getMaxNo());
				appNo.setMaxNo(appNo.getMaxNo()+1);
				appNoMapper.updateByPrimaryKeySelective(this.newSqlParam("appNo", appNo));
				return appNum;
			}else{
				appNo.setMaxNo(2);
				appNo.setRuleDate(sdf.format(new Date()));
				appNoMapper.updateByPrimaryKeySelective(this.newSqlParam("appNo", appNo));
				return appNo.getRuleKey()+appNo.getRuleDate()+String.format("%03d", 1);
			}
		}
	}

}

数据库大致这样:

我用测试类开启了20个线程同时调用这个方法,居然没有重复的申请号,当时还是很郁闷的。

也许是测试类调用不会产生bug,于是我在页面写个按钮,按钮里for循环发5次ajax异步请求,这时就出现bug了,5个请求生成了3个申请号。

我还跟同事讨论这个事,他说是不是事务的问题,我现在才想到,这可能是脏读了。两个业务员同时点击按钮,开启两个线程,都是不同的事务,同时来到这个方法,有synchronized的存在,假如A先执行本方法,B等着,A读到数据库存的是45,然后+1变成46,本方法执行完毕,锁释放,B开始执行,但是由于A后续还有代码在执行,所以A事务没有提交,这时B读到数据库的值仍然是45,接下来+1变成46,于是A和B最后的申请号都是TEST202104010046了

所以说,synchronized其实是起作用的,但是这种场景,它根本锁不住。

于是,我想到用数据库锁。就是本方法体的第一行,查询的selectByPrimaryKey方法sql里直接加for update。

--第一行查询原sql 我们用的mybatis后边是传进去的参数 这里我直接写出来了
select * from APP_NO t WHERE t.rule_key='TEST'
--改成这样
select * from APP_NO t WHERE t.rule_key='TEST' FOR UPDATE 

然后,我试了试,好用!

但是,有问题啊!这相当于使用数据库锁了,此时方法的synchronized完全就是多余的,没它一样能锁住。

并且这样的方式还有一个bug,就是如果将来rule_key字段是首次传进来的,数据库还没有这条记录,那FOR UPDATE根本就锁不住了,这时候仍然会出现脏读,插入两条相同rule_key的数据。我试验了一下,果然主键冲突了(rule_key是表主键)。

那么,此时直接锁整个表应该可以解决此问题,不过这种方式也太损了吧。

 

想了想,又有办法了!让这个生成申请号的方法使用新事务,本方法运行完毕直接提交事务,由于有synchronized修饰,所以只会有一个线程运行到这里。(我们就一个tomcat,不是集群)

我们系统不是用注解控制事务的,而是用spring-mybatis配置文件配置文件名以特定字符开头来控制事务,像add/update/modify之类开头的方法就开启事务propagation="REQUIRED",

那么我就添加一个方法以getOneAppNo开头的就开启新事务propagation="REQUIRES_NEW"。(getOneAppNo就是上边创建申请号的方法)看起来注解方式控制事务才是真好用,我们这个就挺不人性化的。所以,也有类似情况的朋友得看看你的项目是如何开启新事务的。

这样的话,数据库锁就不用了,前边的那个加FOR UPDATE就没用了。

经过验证,效率很好,首次并发创建的申请也没问题。

综上,此问题解决办法就只改一行代码:给这个创建申请号的方法开启新的事务。

synchronized还是没毛病的~~

不过由于我们开启了新的事务,如果调用者的代码出现了异常,我们这里生成的申请号也不会回滚了,会导致号码不连续。听说有的公司的业务要求号码必须连续(好奇葩的要求),那就得想想别的办法了。

 

 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
回答: 在Java中,synchronized关键字可以用来锁住方法,以实现线程同步。当一个线程进入synchronized锁住方法时,其他线程必须等待该线程执行完毕才能进入该方法。此时,锁对象是方法所属的对象实例。例如,引用中的代码片段展示了在synchronized块中使用this作为锁对象。另外,引用展示了在synchronized块中使用实例中的成员变量作为锁对象的例子。还有一种使用synchronized锁住方法的方式是在方法声明中使用synchronized关键字,如引用所示,这种方式相当于锁住的是类对象。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [synchronized锁住的对象](https://blog.csdn.net/llxxjjllll/article/details/120444095)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* [synchronized的几种加锁方式](https://blog.csdn.net/jarniyy/article/details/107954430)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值