高併發程式設計:同步類容器的問題
同步容器類存在的問題
同步類容器都是執行緒安全的,但在某些場景下可能需要加鎖來保護複合操作,在複合操作,如:迭代、跳轉已經條件運算中,這些操作可能會表現出意外的行為,最經典的便是ConcurrentModificationException,原因是當容器迭代的過程中,被併發的修改了內容,這是由於早起迭代器設計的時候並沒有考慮併發修改的原因。
開門見山,我們直接來看兩個例子:
public class UseVector { /** * 遍歷向量 * @return */ public void travelVector(Vector<String> list) { for (String str: list) { System.out.println(str); } } /** * 該方法的主要作用是:從陣列中移除指定的元素 * @param list * @param target * @return 丟擲異常java.util.ConcurrentModificationException */ public Collection<String> removeOne(Vector<String> list, String target) { if(target == null || "".equals(target)) { return list; } //丟擲java.util.ConcurrentModificationException異常 for(String str : list) { if(target.equals(str)) { list.remove(str); } } return list; } public static void main(String[] args) { //定義了一個動態陣列,並向動態陣列中新增3個元素 Vector<String> vector = new Vector<String>(); vector.add("1"); vector.add("2"); vector.add("3"); UseVector demo = new UseVector(); //遍歷這個陣列 demo.travelVector(vector); //從這個動態陣列中移除3這個元素 demo.removeOne(vector, "3"); } }
該案例建立了一個動態陣列Vector並向動態陣列中新增3個元素,同時該類有兩個方法,分別是travelVector()遍歷動態陣列方法和removeOne從動態陣列中移除掉目標元素(target)的方法。執行上訴程式碼,程式丟擲異常:
java.util.ConcurrentModificationException at java.util.Vector$Itr.checkForComodification(Vector.java:1210) at java.util.Vector$Itr.next(Vector.java:1163) at com.springchang.threadcore.test.UseVector.removeOne(UseVector.java:31) at com.springchang.threadcore.test.UseVector.main(UseVector.java:51)
果不其然,該程式碼丟擲了在併發程式設計中常見的ConcurrentModificationException異常,並追蹤上述程式碼得知是removeOne方法中的list.remove(str);這一行丟擲的異常。咱暫時先不管為啥丟擲的異常,先想想有沒有其他代替方案解決我們的需求:遍歷我們的陣列,當陣列元素等於目標值時將其移除。上述迴圈陣列用的是增強for迴圈的方法,我們不妨換成另一種方式,用Iterator迭代器試一試。
public class UseVector { /** * 通過迭起器方法移除一個元素 * @param list * @param target * @return 丟擲異常java.util.ConcurrentModificationException */ public Collection<String> removeOneByIt(Vector<String> list, String target) { if(target == null || "".equals(target)) { return list; } Iterator<String> it = list.iterator(); while(it.hasNext()) { String str = it.next(); if(target.equals(str)) { list.remove(str); } } return list; } public static void main(String[] args) { //定義了一個動態陣列,並向動態陣列中新增3個元素 Vector<String> vector = new Vector<String>(); vector.add("1"); vector.add("2"); vector.add("3"); UseVector demo = new UseVector(); //從這個動態陣列中移除3這個元素 demo.removeOneByIt(vector, "3"); } }
悲傷的是Iterator迭代器並沒有解決我們的問題,同樣的Iterator也丟擲了ConcurrentModificationException異常。在JDK 1.5之前有沒有解決的方案呢?有的,請看以下程式碼:
public class UseVector {
/**
* 安全的從陣列中移除元素的方法
* @param list
* @param target
* @return 不會丟擲丟擲異常java.util.ConcurrentModificationException
*/
public Collection<String> removeOneBySafe(Vector<String> list, String target) {
if(target == null || "".equals(target)) {
return list;
}
//該方法是單現成的,所以安全
for(int i = 0; i < list.size(); i++) {
if(target.equals(list.get(i))) {
list.remove(target);
}
}
return list;
}
public static void main(String[] args) {
//定義了一個動態陣列,並向動態陣列中新增3個元素
Vector<String> vector = new Vector<String>();
vector.add("1");
vector.add("2");
vector.add("3");
UseVector demo = new UseVector();
//從這個動態陣列中移除3這個元素
demo.removeOneBySafe(vector, "3");
System.out.println("移除後的陣列內容:" + vector);
}
}
如上所述,上述程式碼並沒有使用增強的for迴圈來遍歷陣列,也不用Iterator迭代器來遍歷陣列。那麼前兩者與最後一個例子的區別在哪裡呢?這是因為無論是增強的for迴圈還是迭代器在執行的時候迭代器多了一個執行緒來控制遊標位置,控制遊標當前的指向位置,而案例3是單執行緒的,只有一個i下標指向當前元素位置,讀和寫操作都在同一執行緒中完成的,故而安全。
同步類容器的使用
同步類容器如Vector,Hashtable等這些容器的同步功能都是有JDK的Collections.synchronized***等工廠方法去建立實現的。其底層機制用synchronized關鍵字對每個公用的方法都進行同步,或者使用Object mutex物件鎖的機制使得每次只能有一個執行緒訪問容器的狀態。使用的程式碼如下:
public static void main(String[] args) {
//定義了一個動態陣列,並向動態陣列中新增3個元素
Vector<String> vector = new Vector<String>();
vector.add("1");
vector.add("2");
vector.add("3");
UseVector demo = new UseVector();
Collection<String> col = Collections.synchronizedCollection(vector); //先將執行緒不安全的同步類vectory轉為執行緒安全的col
System.out.println(col);
}
要想把同步類變成執行緒安全,先使用Collections.synchronized***方法將vectory轉為執行緒安全的Colection介面的子類,然後在來操作該方法返回的引用,其實現的原理是在底層給物件加鎖。