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要高