1. 程式人生 > >你知道Java中的CopyOnWriteArrayList嗎?

你知道Java中的CopyOnWriteArrayList嗎?

CopyOnWrite

  • CopyOnWrite是什麼?
  • CopyOnWriteArrayList原始碼分享?
  • CopyOnWriteArrayList使用場景?
  • CopyOnWriteArrayList有什麼優缺點?

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

緣由

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

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

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

CopyOnWriteArraySet

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

CopyOnWrite是什麼

官方解釋
CopyOnWriteArrayList 是ArrayList的執行緒安全方式的一種方式。它的add、set方法底層實現都是重新複製一個新的容器來操作的。

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

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

CopyOnWriteArrayList原始碼分享

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.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要高