你知道Java中的CopyOnWriteArrayList嗎?


CopyOnWrite

  • CopyOnWrite是什么?
  • CopyOnWriteArrayList源碼分享?
  • CopyOnWriteArrayList使用場景?
  • CopyOnWriteArrayList有什么優缺點?

如果你是求職者,你想想看怎么回答上面的問題?

緣由

前段時間面試好多個人,問是否用過CopyOnWriteList,發現好多人都沒有用過,感覺挺驚訝的。

CopyOnWrite看字面意思大概就可以明白了,copy集合之后再進行write操作,我們也稱這個為寫時復制容器。

這個從 JDK 1.5版本就已經有了,Java並發包中有兩個實現這個機制的容器,分別是
CopyOnWriteArrayListCopyOnWriteArraySet

CopyOnWrite這個容器非常有用,特別是在並發的時候能夠提升效率,很多並發的的場景中都可以用到CopyOnWrite的容器,我們在生產環境也用到過,今天托尼就和大家順便講講這個容器。

CopyOnWrite是什么

官方解釋
CopyOnWriteArrayList 是ArrayList的線程安全方式的一種方式。它的add、set方法底層實現都是重新復制一個新的容器來操作的。

CopyOnWriteArrayList 與ArrayList不同之處在於添加元素的時候會加上鎖。

CopyOnWriteArrayList在修改容器元素的時候並不是直接在原來的數組上進行修改,它是先拷貝一份,然后在拷貝的數組上進行修改,在修改完成之后將引用賦給原來的數組。

CopyOnWriteArrayList源碼分享

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccessCloneablejava.io.Serializable 
{}
  • 實現了List接口,List的一種實現方式
  • 實現RandomAccess接口,看名稱就知道隨機訪問,和數組訪問一樣根據下標
  • 實現Cloneable接口,代表可以克隆
  • 實現了Serializable接口接口,代表可以被序列化

當容器被初始化添加元素成功之后,多個線程讀取容器中的元素,如果此刻沒有元素的添加,並發多個線程讀取出來的數據大家都是一樣的,可以理解為線程安全的 。

如果此刻有個線程往容器中添加一個新的元素,這個時候CopyOnWriteArrayList就會拷貝一個新的數組出來,將新的元素添加到新的數組中。

在添加元素的這段時間里,如果多線程訪問容器中的元素,將會讀取到舊的數據,等添加元素成功之后會將新的引用地址賦值給舊的list引用地址。

代碼分享:

  • 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);
        return true;
    } finally {
        lock.unlock();
    }
}

大家要注意上面的代碼中ReentrantLock,在添加新元素的時候有加鎖操作,多線程的情況下防止產生臟數據。

  • get方法
public E get(int index{
    return get(getArray(), index);
}

讀的時候不會加鎖,寫的時候會加上鎖,這個時候如果多線程正好寫數據,讀取的時候還是會讀取到舊的數據。

  • set方法
 public E set(int index, E element) {
    //加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //獲取原來數組
        Object[] elements = getArray();
        // 通過索引獲取原來的地址
        E oldValue = get(elements, index);
        // 判斷新舊兩個值是否相等
        if (oldValue != element) {
            int len = elements.length;
            // 拷貝新的數組
            Object[] newElements = Arrays.copyOf(elements, len);
            //根據索引修改元素
            newElements[index] = element;
            // 將原數組的引用指向新數組
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            //為了確保 voliatile 的語義,所以盡管寫操作沒有改變數據,還是調用set方法
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
  • remove方法
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);  
        int numMoved = len - index - 1;  
        if (numMoved == 0)  
            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);  
        }  
        return oldValue;  
    } finally {  
        lock.unlock();  
    }  
}  

同樣也很簡單,都是使用 System.arraycopy、Arrays.copyOf移動元素進行元素的刪除操作。

  • CopyOnWriteArrayList迭代

針對iterator使用了一個叫COWIterator的迭代器,專門針對CopyOnWrite的迭代器,因為不支持寫操作,如上面add、set、remove都會拋出異常,都是不支持的。

/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code remove}
 *         is not supported by this iterator.
 */

public void remove() {
    throw new UnsupportedOperationException();
}

/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code set}
 *         is not supported by this iterator.
 */

public void set(E e) {
    throw new UnsupportedOperationException();
}

/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code add}
 *         is not supported by this iterator.
 */

public void add(E e) {
    throw new UnsupportedOperationException();
}

舉個例子

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
list.add("a");
list.add("b");
list.add("c");

Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
    String next = iterator.next();
       // 這句會報錯的⚠️
    iterator.remove();
}

Exception in thread "main" java.lang.UnsupportedOperationException
    at java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1178)

也正好驗證了迭代的時候UnsupportedOperationException異常。

CopyOnWriteArrayList使用場景

從上面的代碼我們可以看出來了,適用於多讀少寫的場景,比如電商的商品列表,添加新商品和讀取商品就可以用,其他場景小伙伴們可以想想看。

CopyOnWriteArrayList有什么優缺點

缺點:

1、內存占用,因為寫時復制的原理,所以在添加新元素的時候會復制一份,此刻內存中就會有兩份對象,比如這個時候有200M,你在復制一份400M,那么此刻會產生頻繁的JVM的Yong GC和Full GC,
嚴重的會進行STW

Java中Stop-The-World機制簡稱STW,是在執行垃圾收集算法時,Java應用程序的其他所有線程都被掛起(除了垃圾收集幫助器之外)。

2、數據一致性問題,因為CopyOnWrite容器只能保證最終的數據一致性,並不能保證數據的實時性,也就是不具備原子性的效果。

3、數據修改,隨着數組的元素越來越多,修改的時候拷貝數組將會越來越耗時。

優點:

1、多讀少寫,很多時候我們的系統應對的都是讀多寫少的並發場景,讀操作是無鎖操作所以性能較高。

最后說說

  • Vector
  • ArrayList
  • CopyOnWriteArrayList

這三個集合類都繼承List接口

1、ArrayList是線程不安全的

2、Vector是比較古老的線程安全的,但性能不行

3、CopyOnWriteArrayList在兼顧了線程安全的同時,又提高了並發性,性能比Vector要高


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM