title: 单例避免多线程同时修改同个值从而形成脏数据
date: 2017-10-29 13:44:10
tags:git
原文地址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项目连接地址