先簡單說一說Java中的CopyOnWriteArrayList

一、Copy-On-Write 是什麼?

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

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

先給你們一份代碼:數組

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()));
		
	}
}
複製代碼
  • 一、這裏的迭代表示我當前正在讀取某種集合中的數據,屬於操做;
  • 二、線程則模擬當前程序處於多線程環境中,有其餘線程正在修改該數據

這裏暴露的問題是什麼呢?bash

  • 一、多線程會對迭代集合產生影響,影響讀操做

解決:多線程

  • 一、CopyOnWriteArrayList 避免了多線程操做List線程不安全的問題

二、CopyOnWriteArrayList介紹

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

CopyOnWriteArrayList原理:app

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

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

三、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()));
		
	}
}
複製代碼

四、CopyOnWriteArrayList優缺點

缺點:

  • 一、耗內存(集合複製)
  • 二、實時性不高

優勢:

  • 一、數據一致性完整,爲何?由於加鎖了,併發數據不會亂
  • 二、解決了像ArrayListVector這種集合多線程遍歷迭代問題,記住,Vector雖然線程安全,只不過是加了synchronized關鍵字,迭代問題徹底沒有解決!

五、CopyOnWriteArrayList使用場景

  • 一、讀多寫少(白名單,黑名單,商品類目的訪問和更新場景),爲何?由於寫的時候會複製新集合
  • 二、集合不大,爲何?由於寫的時候會複製新集合
  • 實時性要求不高,爲何,由於有可能會讀取到舊的集合數據

參考文章:如何線程安全地遍歷List:Vector、CopyOnWriteArrayList


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

相關文章
相關標籤/搜索