title: 单例避免多线程同时修改同个值从而造成脏数据
date: 2017-10-29 13:44:10
tags:
- singleton
概念
单例模式是一种常用的软件设计模式。单例可以保证系统中一个类只有一个实例,即一个类只有一个对象实例。
优点:
(1)、实例控制
单例会阻止其他对象实例化其自己的对象副本,从而确保所有对象都访问唯一实例。
(2)、节约系统资源
由于系统内存中只存在一个对象,因此可以节约对象频繁创建和销毁。
缺点:
(1)、滥用带来的问题
若单例对象长时间不被利用,系统会认为是垃圾而回收,从而导致对象状态的丢失。此外,如果为了节省资源将数据库连接池对象设计为单例,可能会导致共享连接池对象的程序过多而出现连接池溢出。
(2)、扩展性较差
由于单例模式中没有抽象层,因此扩展有很大的困难。
应用场景
开发过微信公众号的同学应该都接触过微信的AccessToken,正常情况下AccessToken有效期为7200秒,在有效期内重复获取返回相同结果。但是当AccessToken有效期达到临界点时,会存在多个用户访问同个公众号时,同时去修改程序中公众号的AccessToken值,如果处理不当,则会存在AccessToken被多次修改,从而出现AccessToken的脏数据,导致前几次的用户访问出现对应的AccessToken被修改从而出现错误。
实践
环境
JDK 1.8.0_131、MAVEN apache-maven-3.5.0
技术栈
SpringBoot、Mybatis
开发工具
IntelliJ IDEA 2(开发工具大家可以根据自己的喜好而定)
实践过程
一、新建SpringBoot项目,并加入redis依赖:
redis 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>复制代码
依赖说明:redis的引用,在本篇技术中是为了保证单例对象持久化,大家也可以采用直接把Java对象保存在文件中或者在DB中将对象保存起来的方法。出于解决当项目重启时,原先单例对象丢失数据的问题。
二、构建大家的老朋友CalmWangUserModel用户对象,以及对应的Dao层和Service层方法。由于开发环境的约束,无法实现从微信换取AccessToken保存在单例对象中,故在开发环境中采用从DB中拿数据,以模拟完成上述操作。
三、单例对象声明,并编写设置和获取对象属性
本篇中单例的实现是双重校验锁的形式,在JDK1.5之后,双重检查锁定才能够正常达到单例效果。
public class UserSingleton {
private static Logger logger = LoggerFactory.getLogger(UserSingleton.class);
private LinkedHashMap<String, String> linkMap;
public volatile static UserSingleton userSingleton;
private UserSingleton(){}
public LinkedHashMap<String, String> getLinkMap() {
return linkMap;
}
public void setLinkMap(LinkedHashMap<String, String> linkMap) {
this.linkMap = linkMap;
}
}复制代码
代码说明:
(1)、当使用volatile声明的变量的值,系统总是重新从它所在的内存中读取数据,即使它前面的指令刚刚从该处读取过数据。
(2)、LinkedHashMap相对于HashMap的特点就是保存了记录的插入顺序。此处用linkMap是单例对象的一个属性,来保存用户名和联系方式的key,value形式,即模拟存储商家微信公众号的appID和AccessToken值。
public static String getUserSingletonValue(String key){
if(userSingleton == null){
synchronized (UserSingleton.class){
if(userSingleton == null){
userSingleton = new UserSingleton();
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
userSingleton.setLinkMap(map);
}
}
}
logger.info("userSingleton = {}", new Gson().toJson(userSingleton));
return userSingleton.getLinkMap().get(key);
}复制代码
代码说明UserSingleton类方法:
(1)、使用synchronized(同步锁),表示synchronized的代码在执行前必须首先获得UserSingleton类的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,从而保证userSingleton为唯一实例。
(2)、此方法功能主要是通过key,来单例对象中获取对应的value值,如果对象不存在,则创建对象。
public static UserSingleton setUserSingleton(String key, String value){
synchronized (UserSingleton.class){
LinkedHashMap<String, String> map = userSingleton.getLinkMap();
if(StringUtils.isEmpty(map.get(key))){
map.put(key, value);
userSingleton.setLinkMap(map);
}
}
logger.info("userSingleton = {}", new Gson().toJson(userSingleton));
return userSingleton;
}复制代码
代码说明UserSingleton类方法:
synchronized的用法和作用如上,此方法用于将key和value值存入linkmap对象中。
public static LinkedHashMap<String, String> getUserSingleton(){
if(userSingleton == null){
synchronized (UserSingleton.class){
if(userSingleton == null){
userSingleton = new UserSingleton();
LinkedHashMap<String, String> map = new LinkedHashMap<String, String>();
userSingleton.setLinkMap(map);
}
}
}
logger.info("userSingleton = {}", new Gson().toJson(userSingleton));
return userSingleton.getLinkMap();
}复制代码
代码说明UserSingleton类方法:
synchronized的用法和作用如上,此方法用户获取单例中linkmap对象。
四、通过key值获取单例对象对应的value值
@GetMapping("singleton")
public String singleton(String key){
String value = singleApplyService.retun(key);
logger.info("userName = {}", value);
return value;
}复制代码
代码说明:
用户接受请求的控制器,调用service中的逻辑。
public String retun(String key){
//(1)
String value = UserSingleton.getUserSingletonValue(key);
if(StringUtils.isEmpty(value)){
try {
logger.info("in = in");
//(2)
CalmWangUserModel user = calmWangUserService.getByPhone(key);
//(3)
UserSingleton.setUserSingleton(key, user.getUserName());
//(4)
LinkedHashMap<String, String> map = UserSingleton.getUserSingleton();
redisService.setKeyValue("all", map);
value = UserSingleton.getUserSingletonValue(key);
}catch (Exception e){
logger.error("error = {}", e);
}
}
return value;
}复制代码
代码说明:
(1)、通过key值,从单例对象中获取对应的value值,如果不存在则会执行对应的逻辑,如果存在则将value值返回。
(2)、key对应的value值不存在,则从DB中获取对应的信息值,此处预模拟请求微信接口获取对应的AccessToken值。
(3)、将新获取的value值和key值一起保存入单例中。
(4)、为保证单例对象的持久化,故将单例中的linkmap属性值存入redis中,并会创建Bean,当Spring容器在启动时,去注入Bean,将redis中linkmap值存入单例中。(说到Spring容器启动只是一种比较常见单例对象销毁的情况,因为我们在发布项目版本时,这种情况出现的频率还是比较高的)
五、redis持久化获取linkmap值
@Configuration
public class InitUserSingleton {
private static Logger logger = LoggerFactory.getLogger(InitUserSingleton.class);
@Autowired
private RedisServiceI redisService;
@Bean
public UserSingleton init(){
LinkedHashMap<String, String> map = redisService.getMapValue("all");
return UserSingleton.setUserSingletonMap(map);
}
}复制代码
代码说明:
注入Bean,在Sping启动时,从redis中获取linkmap值,并将值传入单例中。
public static UserSingleton setUserSingletonMap(LinkedHashMap<String, String> map){
if(userSingleton == null){
userSingleton = new UserSingleton();
}
userSingleton.setLinkMap(map);
return userSingleton;
}复制代码
代码说明:UserSingleton类方法
设置linkmap属性对应的值
六、测试
我采用的是ab测试,ab -n 4 -c 1 http://localhost:8911/single/singleton?key=136。
四个请求同时发送,查看单例执行情况,大家下载项目,然后运行代码,可以看到 logger.info("in = in");此处信息只打印了一次,由此可以得知除第一次请求外,剩余三次请求都拿到value,从而说明四个请求线程不会同时去操作单例对象,且保证了对象的更新。
总结
此方案适用于独立的微信公众号开发,当开发微信第三方平台时,用此方案存储多个商家的appID和对应的AccessToken时,则会出现线程阻塞等待的情况,原因是单例是独占的,在微信第三方平台环境下,当有多个用户同时进入多个微信公众号,由于单例的特性,会导致部分线程出现阻塞,无法第一时间获取到AccessToken,即使AccessToken存在于linkmap中。
此篇中还有几个问题待解决:
(1)、如何设置单例对象属性值linkmap中value值的过期,此篇开头就提到预解决的问题是微信AccessToken7200秒失效后,获取新的AccessToken,且保证只有单一线程去修改改值,而上述方案中,可以实现单一线程修改值,但未去判断value值是否过期。
(2)、
(3)、此方案的实施,带来的性能问题,这个还有待研究。
我会在后续的文章中,继续跟进上述提到的点。最后也是特别重要的一点,童鞋们如果有更好的理解可以加我微信:wjd632479475,希望能和你认识。
附GitHub项目链接地址