單例避免多線程同時修改同個值從而形成髒數據


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項目連接地址

相關文章
相關標籤/搜索