除了加鎖外,其實還有一種方式可以防止并發修改異常,這就是將讀寫分離技術(不是數據庫上的)。
先回顧一下一個常識:
1、JAVA中“=”操作只是將引用和某個對象關聯,假如同時有一個線程將引用指向另外一個對象,一個線程獲取這個引用指向的對象,那么他們之間不會發生ConcurrentModificationException,他們是在虛擬機層面阻塞的,而且速度非???,幾乎不需要CPU時間。
2、JAVA中兩個不同的引用指向同一個對象,當第一個引用指向另外一個對象時,第二個引用還將保持原來的對象。
基于上面這個常識,我們再來探討下面這個問題:
在CopyOnWriteArrayList里處理寫操作(包括add、remove、set等)是先將原始的數據通過JDK1.6的Arrays.copyof()來生成一份新的數組
然后在新的數據對象上進行寫,寫完后再將原來的引用指向到當前這個數據對象(這里應用了常識1),這樣保證了每次寫都是在新的對象上(因為要保證寫的一致性,這里要對各種寫操作要加一把鎖,JDK1.6在這里用了重入鎖),
然后讀的時候就是在引用的當前對象上進行讀(包括get,iterator等),不存在加鎖和阻塞,針對iterator使用了一個叫 COWIterator的閹割版迭代器,因為不支持寫操作,當獲取CopyOnWriteArrayList的迭代器時,是將迭代器里的數據引用指向當前 引用指向的數據對象,無論未來發生什么寫操作,都不會再更改迭代器里的數據對象引用,所以迭代器也很安全(這里應用了常識2)。
CopyOnWriteArrayList中寫操作需要大面積復制數組,所以性能肯定很差,但是讀操作因為操作的對象和寫操作不是同一個對象,讀之 間也不需要加鎖,讀和寫之間的同步處理只是在寫完后通過一個簡單的“=”將引用指向新的數組對象上來,這個幾乎不需要時間,這樣讀操作就很快很安全,適合 在多線程里使用,絕對不會發生ConcurrentModificationException,所以最后得出結論:CopyOnWriteArrayList適合使用在讀操作遠遠大于寫操作的場景里,比如緩存。
在你的應用中有一個列表(List),它被頻繁的遍歷,但是很少被修改。像“你的主頁上的前十個分類,它被頻繁的訪問,但是每個小時通過Quartz的Job來調度更新”。如果你使用ArrayList來作為該列表的數據結構并且不使用同步(synchronization),你可能會遇到ConcurrentModificationException,因為在你使用Quartz的Job修改該列表時,其他的代碼可能正在遍歷該列表。 有些開發人員可能使用Vector或Collections.synchronizedList(List<T>)的方式來解決該問題。但是這并沒有效果!雖然在列表上add(),remove()和get()方法現在對線程是安全的,但遍歷時仍然會拋出ConcurrentModificationException!在你遍歷在列表時,你需要在該列表上使用同步,同時,在使用Quartz修改它時,也需要使用同步機制。這對性能和可擴展性來說是一個噩夢。同步需要在所有的地方出現,僅僅是因為每個小時都需要做更新。 幸運的是,這里有更好的解決方案。使用CopyOnWriteArrayList。當列表上的一個結構修改發生時,一個新的拷貝(copy)就會被創建。這在經常發生修改的地方使用,將會很低效。遍歷該列表將不會出現ConcurrentModificationException,因為該列表在遍歷時將不會被做任何的修改。另一種避免添加同步代碼但可以避免并發修改問題的方式是在調度任務中構建一個新的列表,然后將原來指向到列表上的引用賦值給新的列表。在JVM中,賦值一個新的引用是原子操作。這種方式在使用舊的遍歷方式(for (int i=0; i<list.size(); i++) { … list.get(i) …})時將無效(也會出錯)。切換的列表中的大小將引發新的錯誤產生。更加糟糕的是因為改變是在不同的線程中發生的,所以還會有很多潛在的問題。使用volatile關鍵字可能會有所幫助,但是對列表大小的改變依然會有問題。 內存一致性和剛發生后保證了CopyOnWriteArrayList的可用性。同時,代碼變得更簡單,因為根本不需要使用volatile關鍵字或同步。更少的代碼,更少的bug! CopyOnWriteArrayList的另一個使用案例是觀察者設計模式。如果事件監聽器由多個不同的線程添加和移除,那么使用CopyOnWriteArrayList將會使得正確性和簡單性得以保證。
新聞熱點
疑難解答