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