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