日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

并发容器CopyOnWriteArrayList

發(fā)布時間:2025/5/22 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 并发容器CopyOnWriteArrayList 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

2019獨角獸企業(yè)重金招聘Python工程師標準>>>

Copy-On-Write簡稱COW,是一種用于程序設計中的優(yōu)化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然后再改,這是一種延時懶惰策略。從JDK1.5開始Java并發(fā)包里提供了兩個使用CopyOnWrite機制實現的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并發(fā)場景中使用到。

CopyOnWrite容器即寫時復制的容器。通俗的理解是當我們往一個容器添加元素的時候,不直接往當前容器添加,而是先將當前容器進行Copy,復制出一個新的容器,然后新的容器里添加元素,添加完元素之后,再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行并發(fā)的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

CopyOnWriteArrayList相當于線程安全的ArrayList,通過增加寫時復制語義來實現線程安全性。底層也是通過一個可變數組來實現的。但是和ArrayList不同的時,它具有以下特性:

  • ?它最適合于具有以下特征的應用程序:List 大小通常保持很小,只讀操作遠多于可變操作,需要在遍歷期間防止線程間的沖突
  • ?支持高效率并發(fā)且是線程安全的
  • ?因為通常需要復制整個基礎數組,所以可變操作(add()、set() 和 remove() 等等)的開銷很大
  • ?迭代器支持hasNext(), next()等不可變操作,但不支持可變 remove()等操作
  • ?使用迭代器進行遍歷的速度很快,并且不會與其他線程發(fā)生沖突。在構造迭代器時,迭代器依賴于不變的數組快照
  • 在使用CopyOnWriteArrayList之前,我們先閱讀其源碼了解下它是如何實現的。以下代碼是向CopyOnWriteArrayList中add方法的實現(向CopyOnWriteArrayList里添加元素),可以發(fā)現在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。

    array: 保存了列表中的數據

    lock: 修改時加鎖,用于保證線程安全

    底層數據結構依然是數組,相比較于ArrayList而言,少了一個表示數組長度的size變量,獲取列表長度是通過下面的方法

    首先來看一下構造函數,如下所示:

    ? ? private volatile transient Object[] array;

    ? ? final Object[] getArray() {
    ? ? ? ? return array;
    ? ? }
    ? ? final void setArray(Object[] a) {
    ? ? ? ? array = a;
    ? ? }
    ? ? // -----開始構造函數----------------------------
    ? ? public CopyOnWriteArrayList() {
    ? ? ? ? setArray(new Object[0]);
    ? ? }
    ? ? public CopyOnWriteArrayList(Collection<? extends E> c) {
    ? ? ? ? Object[] elements = c.toArray();
    ? ? ? ? // c.toArray might (incorrectly) not return Object[] (see 6260652)
    ? ? ? ? if (elements.getClass() != Object[].class)
    ? ? ? ? ? ? elements = Arrays.copyOf(elements, elements.length, Object[].class);
    ? ? ? ? setArray(elements);
    ? ? }
    ? ? // Creates a list holding a copy of the given array.
    ? ? public CopyOnWriteArrayList(E[] toCopyIn) {
    ? ? ? ? setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    ? ? }

    使用一個指向volatile類型的Object數組來保存容器元素。構造函數中都會根據參數值重新生成一個新的數組。

    1.添加元素

    ?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); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 更新底層數組
    ? ? ? ? ? ? return true;
    ? ? ? ? } finally {
    ? ? ? ? ? ? lock.unlock();
    ? ? ? ? }
    ? ? }

    讀的時候不需要加鎖,如果讀的時候有多個線程正在向CopyOnWriteArrayList添加數據,讀還是會讀到舊的數據,因為寫的時候不會鎖住舊的CopyOnWriteArrayList。

    ArrayList新增元素時,可能導致數組擴容;CopyOnWriteArrayList在列表的修改時,采用數組拷貝,在新的數組上進行操作,從這點出發(fā),應該不存在擴容的問題,因為每次修改都會導致數組的重新拷貝

    有兩點必須清楚:

    ?第一,在”添加操作“開始前,獲取獨占鎖(lock),若此時有需要線程要獲取鎖,則必須等待;在操作完畢后,釋放獨占鎖(lock),此時其它線程才能獲取鎖。通過獨占鎖,來防止多線程同時修改數據!

    ?第二,操作完畢時,會通過setArray()來更新volatile數組。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入;這樣,每次添加元素之后,其它線程都能看到新添加的元素。

    2.獲取元素:

    public E get(int index) {
    ? ? return get(getArray(), index);
    }

    private E get(Object[] a, int index) {
    ? ? return (E) a[index];
    }

    將底層volatile數組指定索引處的元素返回即可。

    3.刪除元素:

    以remove(int index)為例,來對“CopyOnWriteArrayList的刪除操作”進行說明。下面是remove(int index)的代碼:

    public E remove(int index) {
    ? ? final ReentrantLock lock = this.lock;
    ? ? lock.lock();
    ? ? try {
    ? ? ? ? Object[] elements = getArray();
    ? ? ? ? int len = elements.length;
    ? ? ? ? E oldValue = get(elements, index); // 獲取volatile數組中指定索引處的元素值
    ? ? ? ? int numMoved = len - index - 1;
    ? ? ? ? if (numMoved == 0) // 如果被刪除的是最后一個元素,則直接通過Arrays.copyOf()進行處理,而不需要新建數組
    ? ? ? ? ? ? setArray(Arrays.copyOf(elements, len - 1));
    ? ? ? ? else {
    ? ? ? ? ? ? Object[] newElements = new Object[len - 1];
    ? ? ? ? ? ? System.arraycopy(elements, 0, newElements, 0, index); ? ?// 拷貝刪除元素前半部分數據到新數組中
    ? ? ? ? ? ? System.arraycopy(elements, index + 1, newElements, index, numMoved);// 拷貝刪除元素后半部分數據到新數組中
    ? ? ? ? ? ? setArray(newElements); // 更新volatile數組
    ? ? ? ? }
    ? ? ? ? return oldValue;
    ? ? } finally {
    ? ? ? ? lock.unlock();
    ? ? }
    }

    從刪除的實現,可確定以下幾點:

    • 修改加鎖,確保同一時刻只有一個線程對數組進行修改

    • 修改并不是在原數組上進行的,而是創(chuàng)建一個新的數組,在新的數組上進行操作操作,然后將tables引用指向新的數組

    • 修改必然會涉及到數組內容的拷貝

    4.遍歷元素:

    public Iterator<E> iterator() {
    ? ? ? ? return new COWIterator<E>(getArray(), 0);
    ? ? }
    ? ? public ListIterator<E> listIterator() {
    ? ? ? ? return new COWIterator<E>(getArray(), 0);
    ? ? }
    ? ? public ListIterator<E> listIterator(final int index) {
    ? ? ? ? Object[] elements = getArray();
    ? ? ? ? int len = elements.length;
    ? ? ? ? if (index<0 || index>len)
    ? ? ? ? ? ? throw new IndexOutOfBoundsException("Index: "+index);

    ? ? ? ? return new COWIterator<E>(elements, index);
    ? ? }

    ? ? private static class COWIterator<E> implements ListIterator<E> {
    ? ? ? ? private final Object[] snapshot; // 保存數組的快照,是一個不可變的對象
    ? ? ? ? private int cursor;

    ? ? ? ? private COWIterator(Object[] elements, int initialCursor) {
    ? ? ? ? ? ? cursor = initialCursor;
    ? ? ? ? ? ? snapshot = elements;
    ? ? ? ? }

    ? ? ? ? public boolean hasNext() {
    ? ? ? ? ? ? return cursor < snapshot.length;
    ? ? ? ? }

    ? ? ? ? public boolean hasPrevious() {
    ? ? ? ? ? ? return cursor > 0;
    ? ? ? ? }

    ? ? ? ? @SuppressWarnings("unchecked")
    ? ? ? ? public E next() {
    ? ? ? ? ? ? if (! hasNext())
    ? ? ? ? ? ? ? ? throw new NoSuchElementException();
    ? ? ? ? ? ? return (E) snapshot[cursor++];
    ? ? ? ? }

    ? ? ? ? @SuppressWarnings("unchecked")
    ? ? ? ? public E previous() {
    ? ? ? ? ? ? if (! hasPrevious())
    ? ? ? ? ? ? ? ? throw new NoSuchElementException();
    ? ? ? ? ? ? return (E) snapshot[--cursor];
    ? ? ? ? }

    ? ? ? ? public int nextIndex() {
    ? ? ? ? ? ? return cursor;
    ? ? ? ? }

    ? ? ? ? public int previousIndex() {
    ? ? ? ? ? ? return cursor-1;
    ? ? ? ? }
    ? ? ? ? public void remove() {
    ? ? ? ? ? ? throw new UnsupportedOperationException();
    ? ? ? ? }
    ? ? ? ? public void set(E e) {
    ? ? ? ? ? ? throw new UnsupportedOperationException();
    ? ? ? ? }
    ? ? ? ? public void add(E e) {
    ? ? ? ? ? ? throw new UnsupportedOperationException();
    ? ? ? ? }
    ? ? }

    如上容器的迭代器中會保存一個不可變的Object數組對象,那么在進行遍歷這個對象時就不需要再進一步的同步。在每次修改時,都會創(chuàng)建并重新發(fā)布一個新的窗口副本,從而實現了可變性。如上迭代器代碼中保留了一個指向volatile數組的引用,由于不會被修改,因此多個線程可以同時對它進行迭代,而不會彼此干擾或與修改容器的線程相互干擾。

    與之前的ArrayList實現相比,CopyOnWriteArrayList返回迭代器不會拋出ConcurrentModificationException異常,即它不是fail-fast機制的!

    ?

    JDK中并沒有提供CopyOnWriteMap,我們可以參考CopyOnWriteArrayList來實現一個,基本代碼如下:

    import?java.util.Collection;

    import?java.util.Map;

    import?java.util.Set;

    public?class?CopyOnWriteMap<K, V>?implements?Map<K, V>, Cloneable {

    ????private?volatile?Map<K, V> internalMap;

    ????public?CopyOnWriteMap() {

    ????????internalMap =?new?HashMap<K, V>();

    ????}

    ????public?V put(K key, V value) {

    ????????synchronized?(this) {

    ????????????Map<K, V> newMap =?new?HashMap<K, V>(internalMap);

    ????????????V val = newMap.put(key, value);

    ????????????internalMap = newMap;

    ????????????return?val;

    ????????}

    ????}

    ????public?V get(Object key) {

    ????????return?internalMap.get(key);

    ????}

    ????public?void?putAll(Map<??extends?K, ??extends?V> newData) {

    ????????synchronized?(this) {

    ????????????Map<K, V> newMap =?new?HashMap<K, V>(internalMap);

    ????????????newMap.putAll(newData);

    ????????????internalMap = newMap;

    ????????}

    ????}

    }

    CopyOnWrite的應用場景

    CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜索網站,用戶在這個網站的搜索框中,輸入關鍵字搜索內容,但是某些關鍵字不允許被搜索。這些不能被搜索的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜索時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜索。實現代碼如下:

    package?com.ifeve.book;

    import?java.util.Map;

    import?com.ifeve.book.forkjoin.CopyOnWriteMap;

    /**

    ?* 黑名單服務

    ?*

    ?* @author fangtengfei

    ?*

    ?*/

    public?class?BlackListServiceImpl {

    ????private?static?CopyOnWriteMap<String, Boolean> blackListMap =?new?CopyOnWriteMap<String, Boolean>(

    ????????????1000);

    ????public?static?boolean?isBlackList(String id) {

    ????????return?blackListMap.get(id) ==?null???false?:?true;

    ????}

    ????public?static?void?addBlackList(String id) {

    ????????blackListMap.put(id, Boolean.TRUE);

    ????}

    ????/**

    ?????* 批量添加黑名單

    ?????*

    ?????* @param ids

    ?????*/

    ????public?static?void?addBlackList(Map<String,Boolean> ids) {

    ????????blackListMap.putAll(ids);

    ????}

    }

    實現很簡單,只要了解了CopyOnWrite機制,我們可以實現各種CopyOnWrite容器,并且在不同的應用場景中使用。

     代碼很簡單,但是使用CopyOnWriteMap需要注意兩件事情:

      1. 減少擴容開銷。根據實際需要,初始化CopyOnWriteMap的大小,避免寫時CopyOnWriteMap擴容的開銷。

      2. 使用批量添加。因為每次添加,容器每次都會進行復制,所以減少添加次數,可以減少容器的復制次數。如使用上面代碼里的addBlackList方法。

    CopyOnWrite的缺點

    CopyOnWrite容器有很多優(yōu)點,但是同時也存在兩個問題,即內存占用問題和數據一致性問題。所以在開發(fā)的時候需要注意一下。

      內存占用問題。因為CopyOnWrite的寫時復制機制,所以在進行寫操作的時候,內存里會同時駐扎兩個對象的內存,舊的對象和新寫入的對象(注意:在復制的時候只是復制容器里的引用,只是在寫的時候會創(chuàng)建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象內存)。如果這些對象占用的內存比較大,比如說200M左右,那么再寫入100M數據進去,內存就會占用300M,那么這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統中使用了一個服務由于每晚使用CopyOnWrite機制更新大對象,造成了每晚15秒的Full GC,應用響應時間也隨之變長。

      針對內存占用問題,可以通過壓縮容器中的元素的方法來減少大對象的內存消耗,比如,如果元素全是10進制的數字,可以考慮把它壓縮成36進制或64進制。或者不使用CopyOnWrite容器,而使用其他的并發(fā)容器,如ConcurrentHashMap。

      數據一致性問題。CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。

    多記錄一句:

    Collections.synchronizedList & CopyOnWriteArrayList

    ? ? ? ?CopyOnWriteArrayList和Collections.synchronizedList是實現線程安全的列表的兩種方式。兩種實現方式分別針對不同情況有不同的性能表現,其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因為是采用了synchronized關鍵字的方式,其讀操作性能并不如CopyOnWriteArrayList。因此在不同的應用場景下,應該選擇不同的多線程安全實現類。

    轉載于:https://my.oschina.net/u/1054538/blog/1594618

    《新程序員》:云原生和全面數字化實踐50位技術專家共同創(chuàng)作,文字、視頻、音頻交互閱讀

    總結

    以上是生活随笔為你收集整理的并发容器CopyOnWriteArrayList的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。