?Copy-On-Write
簡稱COW
辉巡,是一種用于程序設(shè)計(jì)中的優(yōu)化策略太雨。其基本思路是,從一開始大家都在共享同一個(gè)內(nèi)容茫蛹,當(dāng)某個(gè)人想要修改這個(gè)內(nèi)容的時(shí)候操刀,才會(huì)真正把內(nèi)容Copy
出去形成一個(gè)新的內(nèi)容然后再改,這是一種延時(shí)懶惰策略婴洼。從JDK1.5開始Java并發(fā)包里提供了兩個(gè)使用CopyOnWrite
機(jī)制實(shí)現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList
和CopyOnWriteArraySet
骨坑。CopyOnWrite
容器非常有用,可以在非常多的并發(fā)場景中使用到窃蹋。
一. 什么是CopyOnWrite容器
?CopyOnWrite
容器即寫時(shí)復(fù)制的容器卡啰。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候静稻,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進(jìn)行Copy匈辱,復(fù)制出一個(gè)新的容器振湾,然后新的容器里添加元素,添加完元素之后亡脸,再將原容器的引用指向新的容器押搪。這樣做的好處是我們可以對CopyOnWrite
容器進(jìn)行并發(fā)的讀,而不需要加鎖浅碾,因?yàn)楫?dāng)前容器不會(huì)添加任何元素大州。所以CopyOnWrite
容器也是一種讀寫分離的思想,讀和寫不同的容器垂谢。
二. 證明CopyOnWriteArrayList是線程安全的
ReadThread.java:從List中讀取數(shù)據(jù)的線程
import java.util.List;
/**
* <Description> <br>
*
* @author Sunny<br>
* @version 1.0<br>
* @taskId: <br>
* @createDate 2018/08/27 13:14 <br>
* @see com.sunny.jdk.concurrent.cow.copyonwritearraylist <br>
*/
public class ReadThread implements Runnable {
private List<Integer> list;
public ReadThread(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
for (Integer ele : list) {
System.out.println("ReadThread:"+ele);
}
}
}
WriteThread.java:向List中寫數(shù)據(jù)的線程厦画;
/**
* <Description> <br>
*
* @author Sunny<br>
* @version 1.0<br>
* @taskId: <br>
* @createDate 2018/08/27 13:14 <br>
* @see com.sunny.jdk.concurrent.cow.copyonwritearraylist <br>
*/
public class WriteThread implements Runnable {
private List<Integer> list;
public WriteThread(List<Integer> list) {
this.list = list;
}
@Override
public void run() {
Integer num = new Random().nextInt(10);
this.list.add(num);
System.out.println("Write Thread:" + num);
}
}
TestCopyOnWriteArrayList .java:實(shí)現(xiàn)兩個(gè)方法,一個(gè)使用ArrayList容器滥朱,一個(gè)使用CopyOnWriteArrayList容器根暑,來進(jìn)行多線程的讀寫操作;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* <Description> 證明CopyOnWriteArrayList是線程安全的<br>
*
* @author Sunny<br>
* @version 1.0<br>
* @taskId: <br>
* @createDate 2018/08/27 13:14 <br>
* @see com.sunny.jdk.concurrent.cow.copyonwritearraylist <br>
*/
public class TestCopyOnWriteArrayList {
private void testCopyOnWriteArrayList() {
//1徙邻、初始化CopyOnWriteArrayList
List<Integer> tempList = Arrays.asList(new Integer [] {1,2});
CopyOnWriteArrayList<Integer> copyList = new CopyOnWriteArrayList<>(tempList);
//2排嫌、模擬多線程對list進(jìn)行讀和寫
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new ReadThread(copyList));
executorService.execute(new WriteThread(copyList));
executorService.execute(new WriteThread(copyList));
executorService.execute(new WriteThread(copyList));
executorService.execute(new ReadThread(copyList));
executorService.execute(new WriteThread(copyList));
executorService.execute(new ReadThread(copyList));
executorService.execute(new WriteThread(copyList));
executorService.shutdown();
System.out.println("copyList size:"+copyList.size());
}
private void testArrayList() {
//1、初始化CopyOnWriteArrayList
List<Integer> arrList = new ArrayList();
arrList.add(1);
arrList.add(2);
//2缰犁、模擬多線程對list進(jìn)行讀和寫
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService.execute(new ReadThread(arrList));
executorService.execute(new WriteThread(arrList));
executorService.execute(new WriteThread(arrList));
executorService.execute(new WriteThread(arrList));
executorService.execute(new ReadThread(arrList));
executorService.execute(new WriteThread(arrList));
executorService.execute(new ReadThread(arrList));
executorService.execute(new WriteThread(arrList));
executorService.shutdown();
System.out.println("arrList size:"+ arrList.size());
}
public static void main(String[] args) {
TestCopyOnWriteArrayList tcowal = new TestCopyOnWriteArrayList();
//tcowal.testCopyOnWriteArrayList();
tcowal.testArrayList();
}
}
如果調(diào)用tcowal.testCopyOnWriteArrayList();
方法淳地,則會(huì)打印如下:
ReadThread:1
ReadThread:2
copyList size:2
ReadThread:1
ReadThread:2
ReadThread:1
ReadThread:2
Write Thread:5
Write Thread:5
Write Thread:0
Write Thread:2
Write Thread:5
Process finished with exit code 0
如果調(diào)用tcowal.testArrayList();
方法,則會(huì)打印如下:
ReadThread:1
ReadThread:2
arrList size:2
ReadThread:1
Write Thread:8
Write Thread:9
Write Thread:8
ReadThread:1
ReadThread:2
Write Thread:6
Write Thread:9
Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-7" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.sunny.jdk.concurrent.cow.copyonwritearraylist.ReadThread.run(ReadThread.java:23)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.sunny.jdk.concurrent.cow.copyonwritearraylist.ReadThread.run(ReadThread.java:23)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
說明了CopyOnWriteArrayList并發(fā)多線程的環(huán)境下帅容,仍然能很好的工作颇象。
三. CopyOnWriteArrayList的實(shí)現(xiàn)原理
?現(xiàn)在我們來通過看源碼的方式來理解CopyOnWriteArrayList
,實(shí)際上CopyOnWriteArrayList
內(nèi)部維護(hù)的就是一個(gè)數(shù)組并徘,如下:
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
并且該數(shù)組引用是被volatile修飾夯到,注意這里僅僅是修飾的是數(shù)組引用,關(guān)于volatile很重要的一條性質(zhì)是它能夠夠保證可見性饮亏,對list來說,我們自然而然最關(guān)心的就是讀寫的時(shí)候阅爽,分別為get和add方法的實(shí)現(xiàn)路幸。
3.1 get方法實(shí)現(xiàn)原理
?get方法源碼如下:
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
?可以看出來get方法實(shí)現(xiàn)非常簡單,幾乎就是一個(gè)單線程
程序付翁,沒有對多線程添加任何的線程安全控制简肴,也沒有加鎖也沒有CAS操作等等,原因是百侧,所有的讀線程只是會(huì)讀取數(shù)據(jù)容器中的數(shù)據(jù)砰识,并不會(huì)進(jìn)行修改能扒。
3.1 add方法實(shí)現(xiàn)原理
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);
return true;
} finally {
lock.unlock();
}
}
/**
* Inserts the specified element at the specified position in this
* list. Shifts the element currently at that position (if any) and
* any subsequent elements to the right (adds one to their indices).
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
add方法的邏輯也比較容易理解,請看上面的注釋辫狼。需要注意這么幾點(diǎn):
- 采用
ReentrantLock
初斑,保證同一時(shí)刻只有一個(gè)寫線程正在進(jìn)行數(shù)組的復(fù)制,否則的話內(nèi)存中會(huì)有多份被復(fù)制的數(shù)據(jù)膨处; - 前面說過數(shù)組引用是
volatile
修飾的见秤,因此將舊的數(shù)組引用指向新的數(shù)組,根據(jù)volatile
的happens-before
規(guī)則真椿,寫線程對數(shù)組引用的修改對讀線程是可見的鹃答。 - 由于在寫數(shù)據(jù)的時(shí)候,是在新的數(shù)組中插入數(shù)據(jù)的突硝,從而保證讀寫是在兩個(gè)不同的數(shù)據(jù)容器中進(jìn)行操作测摔。
這里還有這樣一個(gè)問題: 為什么需要復(fù)制呢? 如果將array
數(shù)組設(shè)定為volatile
的解恰, 對volatile
變量寫happens-before
讀锋八,讀線程不是能夠感知到volatile
變量的變化。
原因是修噪,這里volatile
的修飾的僅僅只是數(shù)組引用查库,數(shù)組中的元素的修改是不能保證可見性的。因此COW
采用的是新舊兩個(gè)數(shù)據(jù)容器黄琼,通過setArray(newElements);
這一行代碼將數(shù)組引用指向新的數(shù)組樊销。
四. CopyOnWrite的使用場景
CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場景。比如白名單脏款,黑名單围苫,商品類目的訪問和更新場景,假如我們有一個(gè)搜索網(wǎng)站撤师,用戶在這個(gè)網(wǎng)站的搜索框中剂府,輸入關(guān)鍵字搜索內(nèi)容,但是某些關(guān)鍵字不允許被搜索剃盾。這些不能被搜索的關(guān)鍵字會(huì)被放在一個(gè)黑名單當(dāng)中腺占,黑名單每天晚上更新一次。當(dāng)用戶搜索時(shí)痒谴,會(huì)檢查當(dāng)前關(guān)鍵字在不在黑名單當(dāng)中衰伯,如果在,則提示不能搜索积蔚。實(shí)現(xiàn)代碼如下:
import java.util.Map;
import com.ifeve.book.forkjoin.CopyOnWriteMap;
/**
* 黑名單服務(wù)
*
*
*/
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);
}
}
代碼很簡單意鲸,但是使用CopyOnWriteMap
需要注意兩件事情:
減少擴(kuò)容開銷。根據(jù)實(shí)際需要,初始化
CopyOnWriteMap
的大小怎顾,避免寫時(shí)CopyOnWriteMap
擴(kuò)容的開銷读慎。使用批量添加。因?yàn)槊看翁砑踊蔽恚萜髅看味紩?huì)進(jìn)行復(fù)制夭委,所以減少添加次數(shù),可以減少容器的復(fù)制次數(shù)蚜退。如使用上面代碼里的
addBlackList
方法闰靴。
五. CopyOnWriteArrayList的缺點(diǎn)
?CopyOnWrite容器有很多優(yōu)點(diǎn),但是同時(shí)也存在兩個(gè)問題钻注,即內(nèi)存占用問題和數(shù)據(jù)一致性問題蚂且。所以在開發(fā)的時(shí)候需要注意一下。
內(nèi)存占用問題幅恋。因?yàn)?code>CopyOnWrite的寫時(shí)復(fù)制機(jī)制杏死,所以在進(jìn)行寫操作的時(shí)候,內(nèi)存里會(huì)同時(shí)駐扎兩個(gè)對象的內(nèi)存捆交,舊的對象和新寫入的對象(注意:在復(fù)制的時(shí)候只是復(fù)制容器里的引用淑翼,只是在寫的時(shí)候會(huì)創(chuàng)建新對象添加到新容器里,而舊容器的對象還在使用品追,所以有兩份對象內(nèi)存)玄括。如果這些對象占用的內(nèi)存比較大,比如說200M左右肉瓦,那么再寫入100M數(shù)據(jù)進(jìn)去遭京,內(nèi)存就會(huì)占用300M,那么這個(gè)時(shí)候很有可能造成頻繁的Yong GC和Full GC泞莉。之前我們系統(tǒng)中使用了一個(gè)服務(wù)由于每晚使用CopyOnWrite
機(jī)制更新大對象哪雕,造成了每晚15秒的Full GC,應(yīng)用響應(yīng)時(shí)間也隨之變長鲫趁。
針對內(nèi)存占用問題斯嚎,可以通過壓縮容器中的元素的方法來減少大對象的內(nèi)存消耗,比如挨厚,如果元素全是10進(jìn)制的數(shù)字堡僻,可以考慮把它壓縮成36進(jìn)制或64進(jìn)制∫咛辏或者不使用CopyOnWrite
容器苦始,而使用其他的并發(fā)容器,如ConcurrentHashMap
數(shù)據(jù)一致性問題慌申。CopyOnWrite
容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。所以如果你希望寫入的的數(shù)據(jù)蹄溉,馬上能讀到咨油,請不要使用CopyOnWrite
容器。
六. 對比Collections.synchronizedList
CopyOnWriteArrayList
和Collections.synchronizedList
是實(shí)現(xiàn)線程安全的列表的兩種方式柒爵。兩種實(shí)現(xiàn)方式分別針對不同情況有不同的性能表現(xiàn)役电。
因?yàn)?code>CopyOnWriteArrayList的寫操作不僅有lock
鎖,還在內(nèi)部進(jìn)行了數(shù)組的copy
棉胀,所以性能比Collections.synchronizedList
要低法瑟。
而讀操作CopyOnWriteArrayList
直接取的數(shù)組的值,Collections.synchronizedList
卻有synchronized
修飾唁奢,所以讀性能CopyOnWriteArrayList
略勝一籌霎挟。
因此在不同的應(yīng)用場景下,應(yīng)該選擇不同的多線程安全實(shí)現(xiàn)類麻掸。
七. COW vs 讀寫鎖
相同點(diǎn):1. 兩者都是通過讀寫分離的思想實(shí)現(xiàn)酥夭;2.讀線程間是互不阻塞的
不同點(diǎn):對讀線程而言,為了實(shí)現(xiàn)數(shù)據(jù)實(shí)時(shí)性脊奋,在寫鎖被獲取后熬北,讀線程會(huì)等待或者當(dāng)讀鎖被獲取后,寫線程會(huì)等待诚隙,從而解決“臟讀”等問題讶隐。也就是說如果使用讀寫鎖依然會(huì)出現(xiàn)讀線程阻塞等待的情況。而COW則完全放開了犧牲數(shù)據(jù)實(shí)時(shí)性而保證數(shù)據(jù)最終一致性久又,即讀線程對數(shù)據(jù)的更新是延時(shí)感知的巫延,因此讀線程不會(huì)存在等待的情況。
八. CopyOnWriteArrayList透露的思想
?如上面的分析CopyOnWriteArrayList
表達(dá)的一些思想:
- 讀寫分離籽孙,讀和寫分開
- 最終一致性
- 使用另外開辟空間的思路烈评,來解決并發(fā)沖突