环境
java:1.7
场景
公司一项目有个疑难问题:
如上图,当狂点按钮时,会发送很多请求;而这样会造成 加入的数据会出现重复;
虽然可以叫前端去控制,但是这其实应该在后端进行控制,因为绕过前端js
并不是一件很难的事情。
我们先来分析下,会出现重复的原因:
1、首先我们先来看看代码:
//其他代码省略...
if (fullCodes.contains(fullCode)) {
fullCodes.remove(fullCode);
}
//其他逻辑代码省略...
collection.insert(values);
//其他代码省略...
我这里贴出来的是核心的代码。
正常情况下,当请求过来时,fullCodes.contains(fullCode)
这段代码会做去重处理。在去完重复后,再去执行插入
操作。
在狂点而产生的高并发下,如上图,一瞬间有4个请求过来了;这时,当第一个请求执行完fullCodes.contains(fullCode)
这段代码后,去执行其他逻辑是,但还没有执行insert
操作,第二个请求也开始执行fullCodes.contains(fullCode)
这段代码,这就是造成重复的原因。
简单点讲就是,第一个请求还没有执行完,第二个请求已经开始执行了!导致上面写的去重逻辑没起到作用!
2、业务层面上,其就是添加自选股的操作!这里之所以使用insert
而不是update
,因为其需要支持大量股票的添加,update
的话,会很慢;
3、场景:并发是必须支持的,只是当一个用户进行添加而产生的并发时,必须串行执行。
知道了原因和需求后,就好办了!
解决方案
synchronized
最开始我想到是使用一个共享变量,或者使用volatile
;
但是我这情况,并不是因为两个线程读取某个变量有差异而造成的错误。
我这边的需求是,去重逻辑和insert
操作,必须同时执行,即必须是个原子操作!
也就是代码块上必须加把锁,保持串行;
第一次代码:
synchronized(""){
//其他代码省略...
if (fullCodes.contains(fullCode)) {
fullCodes.remove(fullCode);
}
//其他逻辑代码省略...
collection.insert(values);
//其他代码省略...
}
结果 没有控制住;也就是每个线程都创建了一个""
字符串。我以为编译时,会是一个常量,无论哪个线程得到的都是一样的。
第二次修改:
synchronized(accountId.toString()){
//其他代码省略...
if (fullCodes.contains(fullCode)) {
fullCodes.remove(fullCode);
}
//其他逻辑代码省略...
collection.insert(values);
//其他代码省略...
}
结果呢!还是没有控制住,原因还是和上面一样,每次请求进来时,accountId.toString()
都创建了一个字符串对象,而synchronized
锁的就是后面括号里的对象,因为每次进来都不是同一个对象,所以就没有效果。
第三次修改:
synchronized(accountId.toString().intern()){
//其他代码省略...
if (fullCodes.contains(fullCode)) {
fullCodes.remove(fullCode);
}
//其他逻辑代码省略...
collection.insert(values);
//其他代码省略...
}
也就是使用intern()
方法; 结果:生效了
。
intern
方法稍后讲解;
测试
1、狂点按钮,依然会发送多个请求(毕竟前端没有处理),看看数据,没有重复。OK,控制住了
2、两个账号,即我和同事,同时进行狂点按钮;查看数据,OK,没有重复,并且是并发执行(我的操作和同事的操作)。
知识点
synchronized
网上摘抄:
1、修饰方法:synchronized关键字修饰的方法。
由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,
内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态
代码如: public synchronized void save(){}
2、修饰代码块:synchronized关键字修饰的语句块。
被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
synchronized(object){ }
3、synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
这里我使用的是第二种,锁住的是代码块。特别需要注意的地方就是,这种方式,锁的是括号里面的对象;
当你发现自己写的代码视乎没有锁住时,肯定是因为两个线程得到的对象不是同一个造成的!
而要保证两个线程得到同一个对象,如果是字符串应该使用intern
方法,如果是引用对象,应该使用枚举;
intern
intern
这个方法我曾经写过类似的博文,那时我没有实际实战过,只是有有关的理论知识,知道这么做,会使得在首次在堆上创建的字符串,其引用会记录到常量池
中去。这样以后拿到的都是同一个字符串。
今天算是,真正实战了下;
String.intern()方法与常量池存入时的疑惑!
建议加个前缀
我最终的代码是这样的:
synchronized(SynPrefix.addMyStocks + accountId.toString().intern()){
//其他代码省略...
if (fullCodes.contains(fullCode)) {
fullCodes.remove(fullCode);
}
//其他逻辑代码省略...
collection.insert(values);
//其他代码省略...
}
SynPrefix
类代码:
public enum SynPrefix {
// ggservice.v1.mystock.service.MyStockService.addMyStocks
addMyStocks,
// ggservice.v1.mystock.service.MyStockGroupService.addGroup
addGroup;
}
之所以这么做,因为如果仅仅使用accountId
作为锁的话,那么假设有人也在其他类似的地方加了一个锁,也是用accountId
,这样的话,会造成,其他地方的代码也会被锁住。
所以我已方法名为前缀,并写出枚举;