單例避免多執行緒同時修改同個值從而造成髒資料

yunkai發表於2017-10-30

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專案連結地址

相關文章