单例避免多线程同时修改同个值从而造成脏数据


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项目链接地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值