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


title: 单例避免多线程同时修改同个值从而形成脏数据
date: 2017-10-29 13:44:10
tags:git

- singleton

原文地址github

概念

单例模式是一种经常使用的软件设计模式。单例能够保证系统中一个类只有一个实例,即一个类只有一个对象实例。
优势:
   (1)、实例控制
      单例会阻止其余对象实例化其本身的对象副本,从而确保全部对象都访问惟一实例。
   (2)、节约系统资源
      因为系统内存中只存在一个对象,所以能够节约对象频繁建立和销毁。
缺点:
   (1)、滥用带来的问题
      若单例对象长时间不被利用,系统会认为是垃圾而回收,从而致使对象状态的丢失。此外,若是为了节省资源将数据库链接池对象设计为单例,可能会致使共享链接池对象的程序过多而出现链接池溢出。
   (2)、扩展性较差
因为单例模式中没有抽象层,所以扩展有很大的困难。redis

应用场景

开发过微信公众号的同窗应该都接触过微信的AccessToken,正常状况下AccessToken有效期为7200秒,在有效期内重复获取返回相同结果。可是当AccessToken有效期达到临界点时,会存在多个用户访问同个公众号时,同时去修改程序中公众号的AccessToken值,若是处理不当,则会存在AccessToken被屡次修改,从而出现AccessToken的脏数据,致使前几回的用户访问出现对应的AccessToken被修改从而出现错误。spring

实践

环境

JDK 1.8.0_13一、MAVEN apache-maven-3.5.0数据库

技术栈

SpringBoot、Mybatisapache

开发工具

IntelliJ IDEA 2(开发工具你们能够根据本身的喜爱而定)设计模式

实践过程

1、新建SpringBoot项目,并加入redis依赖:
redis 依赖bash

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>复制代码

依赖说明:redis的引用,在本篇技术中是为了保证单例对象持久化,你们也能够采用直接把Java对象保存在文件中或者在DB中将对象保存起来的方法。出于解决当项目重启时,原先单例对象丢失数据的问题。微信

2、构建你们的老朋友CalmWangUserModel用户对象,以及对应的Dao层和Service层方法。因为开发环境的约束,没法实现从微信换取AccessToken保存在单例对象中,故在开发环境中采用从DB中拿数据,以模拟完成上述操做。多线程

3、单例对象声明,并编写设置和获取对象属性
本篇中单例的实现是双重校验锁的形式,在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对象。

4、经过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容器启动只是一种比较常见单例对象销毁的状况,由于咱们在发布项目版本时,这种状况出现的频率仍是比较高的)

5、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属性对应的值

6、测试
我采用的是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项目连接地址

相关文章
相关标签/搜索