java 【synchronized】简单应用

环境

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,这样的话,会造成,其他地方的代码也会被锁住。
所以我已方法名为前缀,并写出枚举;

参考地址:

https://www.cnblogs.com/XHJT/p/3897440.html

记一次synchronized锁字符串引发的坑兼再谈Java字符串

java synchronized锁对象,但是当对象引用是null的时候,锁的是什么?

java多线程在单核CPU上,还是需要volatile synchronized吗?

猜你喜欢

转载自blog.csdn.net/u013066244/article/details/80538478