先簡單說一說Java中的CopyOnWriteArrayList

擁抱心中的夢想發表於2018-03-15

1、Copy-On-Write 是什麼?

首先我講一下什麼是Copy-On-Write,顧名思義,在計算機中就是當你想要對一塊記憶體進行修改時,我們不在原有記憶體塊中進行操作,而是將記憶體拷貝一份,在新的記憶體中進行操作,完之後呢,就將指向原來記憶體指標指向新的記憶體,原來的記憶體就可以被回收掉嘛!

網上兄弟們說了,這是一種用於程式設計中的優化策略,是一種延時懶惰策略。都說優化優化,那麼到底優化了哪些問題呢?

先給大家一份程式碼:

public class IteratorTest {

	private static List<String> list = new ArrayList<>();

	public static void main(String[] args) {
		
		list.add("1");
		list.add("2");
		list.add("3");
		
		Iterator<String> iter = list.iterator();
		
		//我當前正在迭代集合(這裡模擬併發中讀取某一list的場景)
		while (iter.hasNext()) {
			
			System.err.println(iter.next());
		
		}
		
		System.err.println(Arrays.toString(list.toArray()));
	}
}
複製程式碼

上面的程式片段在單執行緒下執行時沒什麼毛病的,但到了多執行緒的環境中,可能就GG了!為什麼呢?因為多執行緒環境中,你在迭代的時候是不允許有其他執行緒對這個集合list進行新增元素的,看下面這段程式碼,你會發現丟擲java.util.ConcurrentModificationException的異常。

public class IteratorTest {

	private static List<String> list = new ArrayList<>();

	public static void main(String[] args) {

		list.add("1");
		list.add("2");
		list.add("3");

		Iterator<String> iter = list.iterator();

		// 存放10個執行緒的執行緒池
		ExecutorService service = Executors.newFixedThreadPool(10);

		// 執行10個任務(我當前正在迭代集合(這裡模擬併發中讀取某一list的場景))
		for (int i = 0; i < 10; i++) {
			service.execute(new Runnable() {
				@Override
				public void run() {
					while (iter.hasNext()) {
						System.err.println(iter.next());
					}
				}
			});
		}
		
		// 執行10個任務
		for (int i = 0; i < 10; i++) {
			service.execute(new Runnable() {
				@Override
				public void run() {
					list.add("121");// 新增資料
				}
			});
		}
		
		System.err.println(Arrays.toString(list.toArray()));
		
	}
}
複製程式碼
  • 1、這裡的迭代表示我當前正在讀取某種集合中的資料,屬於操作;
  • 2、執行緒則模擬當前程式處於多執行緒環境中,有其他執行緒正在修改該資料

這裡暴露的問題是什麼呢?

  • 1、多執行緒會對迭代集合產生影響,影響讀操作

解決:

  • 1、CopyOnWriteArrayList 避免了多執行緒操作List執行緒不安全的問題

2、CopyOnWriteArrayList介紹

從JDK1.5開始Java併發包裡提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayListCopyOnWriteArraySetCopyOnWrite容器非常有用,可以在非常多的併發場景中使用到。

CopyOnWriteArrayList原理:

上面已經講了,就是在寫的時候不對原集合進行修改,而是重新複製一份,修改完之後,再移動指標
複製程式碼

那麼你可能會問?就算是對原集合進行復制,在多執行緒環境中不也是一樣會導致寫入衝突嗎?沒錯,但是你可能還不知道CopyOnWriteArrayList中增加刪除元素的實現細節,下面我就說說網上老是提到的add()方法

3、CopyOnWriteArrayList簡單原始碼解讀

add()方法原始碼:

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;//重入鎖
        lock.lock();//加鎖啦
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);//拷貝新陣列
            newElements[len] = e;
            setArray(newElements);//將引用指向新陣列  1
            return true;
        } finally {
            lock.unlock();//解鎖啦
        }
    }
複製程式碼

恍然大悟,小樣,原來add()在新增集合的時候加上了鎖,保證了同步,避免了多執行緒寫的時候會Copy出N個副本出來。(想想,你在遍歷一個10個元素的集合,每遍歷一次有1人呼叫add方法,你說當你遍歷10次,這add方法是不是得被呼叫10次呢?是不是得copy出10分新集合呢?萬一這個集合非常大呢?)

那麼?你還要問?CopyOnWriteArrayList是怎麼解決執行緒安全問題的?答案就是----寫時複製,加鎖 還要問?那麼有沒有這麼一種情況,當一個執行緒剛好呼叫完add()方法,也就是剛好執行到上面1處的程式碼,也就是剛好將引用指向心陣列,而此時有執行緒正在遍歷呢?會不會報錯呢?(答案是不會的,因為你正在遍歷的集合是舊的,這就有點難受啦,哈哈~

當你把上面的程式碼的ArrayList改為CopyOnWriteArrayList,執行就不會報錯啦!

public class IteratorTest {

	private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

	public static void main(String[] args) {

		list.add("1");
		list.add("2");
		list.add("3");

		Iterator<String> iter = list.iterator();

		// 存放10個執行緒的執行緒池
		ExecutorService service = Executors.newFixedThreadPool(10);

		// 執行10個任務(我當前正在迭代集合(這裡模擬併發中讀取某一list的場景))
		for (int i = 0; i < 10; i++) {
			service.execute(new Runnable() {
				@Override
				public void run() {
					while (iter.hasNext()) {
						System.err.println(iter.next());
					}
				}
			});
			service.execute(new Runnable() {
				@Override
				public void run() {
					list.add("121");// 新增資料
				}
			});
		}
		
		// 執行10個任務
		for (int i = 0; i < 10; i++) {
			service.execute(new Runnable() {
				@Override
				public void run() {
					list.add("121");// 新增資料
				}
			});
			service.execute(new Runnable() {
				@Override
				public void run() {
					while (iter.hasNext()) {
						System.err.println(iter.next());
					}
				}
			});
		}
		
		System.err.println(Arrays.toString(list.toArray()));
		
	}
}
複製程式碼

4、CopyOnWriteArrayList優缺點

缺點:

  • 1、耗記憶體(集合複製)
  • 2、實時性不高

優點:

  • 1、資料一致性完整,為什麼?因為加鎖了,併發資料不會亂
  • 2、解決了像ArrayListVector這種集合多執行緒遍歷迭代問題,記住,Vector雖然執行緒安全,只不過是加了synchronized關鍵字,迭代問題完全沒有解決!

5、CopyOnWriteArrayList使用場景

  • 1、讀多寫少(白名單,黑名單,商品類目的訪問和更新場景),為什麼?因為寫的時候會複製新集合
  • 2、集合不大,為什麼?因為寫的時候會複製新集合
  • 實時性要求不高,為什麼,因為有可能會讀取到舊的集合資料

參考文章:如何執行緒安全地遍歷List:Vector、CopyOnWriteArrayList


小編大四,正在實習,學識尚淺,歡迎評論交流,一起學習,一起進步!

相關文章