轉載自:http://kingj.iteye.com/blog/1452427
除了加鎖外,其實還有一種方式可以防止并發修改異常,這就是将讀寫分離技術(不是資料庫上的)。
先回顧一下一個常識:
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将會使得正确性和簡單性得以保證。
這個類不能用下标去周遊,而要用iterator.
如果用下标周遊的話,與此同時另一個線程去修改了COW,那麼可能會報下标out of array.
code:
Java代碼
- public class COWT {
- public static void main(String...args) throws InterruptedException
- {
- final CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>();
- for(int i=0;i<10;i++)
- {
- cowList.add(i);
- }
- new Thread(){
- @Override
- public void run() {
- for(int i=0;i<cowList.size();i++)
- {
- try {
- Thread.currentThread().sleep(1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(cowList.get(i));
- }
- // Iterator<Integer> it = cowList.iterator();
- // while(it.hasNext())
- // {
- // try {
- // Thread.currentThread().sleep(1);
- // } catch (InterruptedException e) {
- // e.printStackTrace();
- // }
- // System.out.println(it.next());
- // }
- };
- }.start();
- new Thread(){
- @Override
- public void run() {
- try {
- Thread.currentThread().sleep(3);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- cowList.clear();
- };
- }.start();
- }
- }
用copyOnWriteList在資料量較大的時候,性能會下降很厲害。
每次的add都會資料拷貝而ArrayList不會,性能較高
我一般的解決方案,還是引用切換的原子操作
另一種避免添加同步代碼但可以避免并發修改問題的方式是在排程任務中建構一個新的清單,然後将原來指向到清單上的引用指派給新的清單。在JVM中,指派一個新的引用是原子操作。這種方式在使用舊的周遊方式(for (int i=0; i<list.size(); i++) { … list.get(i) …})時将無效(也會出錯)。切換的清單中的大小将引發新的錯誤産生。更加糟糕的是因為改變是在不同的線程中發生的,是以還會有很多潛在的問題。使用volatile關鍵字可能會有所幫助,但是對清單大小的改變依然會有問題。
List old = new ArrayList();
List temp = old;
for(int i=0;i<temp.size;i++){
...temp.get(i)
}
或iterator的周遊