單例模式在高併發情形下造成的訪問覆蓋問題
好吧,最近我特麼是跟高併發槓上了。。
單例模式想必很很常見,而往往單例模式跟static相關。單例模式的初衷是為了在任何條件下我只得到一個例項,包括類和變數。而往往需要我們用static關鍵字去修飾達到單例的效果。最近高併發接觸得比較多,使用快取就需要用單例。因為你針對某一個key的快取只可能定義成“一份”。所以快取類的例項需要用到單例模式。但是在高併發的條件下,控制不好的話,很容易出問題。下面寫個小例子,就能看出是什麼問題了……
@Controller public class TestAction { @RequestMapping("/test/context.json") @ResponseBody public void test() { Thread t = Thread.currentThread(); new Thread(new TestThread("count")).start(); try { t.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(new TestThread("count")).start(); } } class TestThread implements Runnable{ private String attr; public TestThread(String attr) { this.attr = attr; } @Override public void run() { List<String> list = Test3.getList(attr); list.add("d"); System.out.println(list); System.out.println("==========="); } }
這裡用啟動兩個執行緒TestThread模擬“併發”。
而我們再模擬使用到單例模式的情形:
public class Test3 { private static List<String> list = new ArrayList<String>(); public static List<String> getList(String attr) { WebApplicationContext wc = ContextLoader.getCurrentWebApplicationContext(); //這裡用到ServletContext模擬快取的情況 ServletContext sc = wc.getServletContext(); String count = (String) sc.getAttribute(attr); if(StringUtils.equals("1", count)) { //啥也不做 }else{ sc.setAttribute(attr, "1"); list.add("a"); list.add("b"); list.add("c"); } return list; } }
其中list是static的全域性變數。這裡用ServletContext的特性模擬了快取的情況。
看看TestAction中定義的執行緒TestThread,該執行緒被啟動了2次,(模擬併發),並且2次都是傳入同一個引數(模擬相同條件)"count"。
瀏覽器輸入TestAction註解的Url,可發現控制檯列印如下:
[a, b, c, d]
===========
[a, b, c, d, d]
===========
再次輸入該Url,列印如下:
[a, b, c, d, d, d]
===========
[a, b, c, d, d, d, d]
===========
問題已很明顯了,執行緒第一次執行時,集合本來為[a.b.c]被它修改(add("d"))之後,集合被覆蓋為[a,b,c,d]了;同理,第二次輸入Url之後,集合又被執行緒第二次執行時覆蓋為[a,b,c,d,d]了·,所以此次在進行add("d")操作之後,集合被覆蓋為[a,b,c,d,d,d]啦,以此類推……
其實這種問題是比較容易被忽視的,併發條件下,你對一個“公共”的變數(一般是由static修飾),常見場景如快取的操作(這裡是add("d"))修改,會不斷更新【最初】的變數值,【新】的執行緒再次訪問時,得到的已經不是【最初】的值了。這顯然是不對的,我們需要做到對一個公共變數進行多執行緒訪問時,執行緒與執行緒之間的訪問不彼此影響,即:執行緒不會修改公共的變數值,不影響其他執行緒的訪問。
注意:需要注意這種情況只涉及到執行緒需要對拿到的公共變數修改時,純讀取的話,沒必要注意這個問題。
如何解決呢?我們只需拷貝一個公共變數的“副本”,即可達到想要的效果:
改變Test3的方法如下:
public static List<String> getList(String attr) {
WebApplicationContext wc = ContextLoader.getCurrentWebApplicationContext();
ServletContext sc = wc.getServletContext();
String count = (String) sc.getAttribute(attr);
if(StringUtils.equals("1", count)) {
//啥也不做
}else{
sc.setAttribute(attr, "1");
list.add("a");
list.add("b");
list.add("c");
}
List<String> copyList = new ArrayList<String>(list);
return copyList;
}
copyList是公共變數的副本,這樣,當有N個執行緒去訪問公共變數時,得到的是副本,你之後再對該副本進行任何操作,都不會影響公共變數,從而不影響其他執行緒對該公共變數的訪問,確保其他執行緒拿到的都是【最初】的公共變數。
同樣,訪問Url,
列印如下:
[a, b, c, d]
===========
[a, b, c, d]
===========
再次訪問:
[a, b, c, d]
===========
[a, b, c, d]
===========
說明:問題解決。