Java集合--線程安全(CopyOnWrite機(jī)制)

5 Java并發(fā)集合

5.1 引言

在前幾章中,我們介紹了Java集合的內(nèi)容,具體包括ArrayList、HashSet、HashMap、ArrayQueue等實(shí)現(xiàn)類。

不知道各位有沒有發(fā)現(xiàn),上述集合都有一個共同的特點(diǎn),那就是線程不安全性,在并發(fā)情況下都不能保證數(shù)據(jù)的一致性。(當(dāng)然,這個集合必須是共享了,所以才會有數(shù)據(jù)不一致)

所以,當(dāng)我們在進(jìn)行并發(fā)任務(wù)時候,共享了一個不適用于并發(fā)的數(shù)據(jù)結(jié)構(gòu),也就是將此數(shù)據(jù)結(jié)構(gòu)變成了程序中的成員變量,那么我們將會遇到數(shù)據(jù)的不一致,進(jìn)而影響到我們程序的運(yùn)行。

為了應(yīng)對并發(fā)場景的出現(xiàn),Java在后續(xù)迭代過程中(具體應(yīng)該是JDK1.5版本),推出了java.util.concurrent包。該包的出現(xiàn),讓Java并發(fā)編程變得更加輕松,幫助開發(fā)者編寫更加高效、易維護(hù)、結(jié)構(gòu)清晰的程序。

在java.util.concurrent包中,不但包含了我們本篇要說的線程安全的集合,還涉及到了多線程、CAS、線程鎖等相關(guān)內(nèi)容,可以說是完整覆蓋了Java并發(fā)的知識棧。

對于Java開發(fā)人員來說,學(xué)好java.util.concurrent包下的內(nèi)容,是一個必備的功課,也是逐漸提升自己的一個重要階段。

5.2 并發(fā)集合實(shí)現(xiàn)1

JDK1.5的出現(xiàn),對于集合并發(fā)編程來說,java developer有了更多的選擇。不過,在JDK1.5之前,Java也還是提供了一些解決方案。

(1)最為簡單直接的就是在程序中我們自己對共享變量進(jìn)行加鎖。不過,缺點(diǎn)也顯而易見,手動實(shí)現(xiàn)線程安全間接增加了程序的復(fù)雜度,以及代碼出錯的概率---例如:線程死鎖的產(chǎn)生;

(2)我們還可以使用Java集合框架中的Vector、Hashtable實(shí)現(xiàn)類,這兩個類都是線程安全的。不過,Java已不提倡使用。

(3)此外,我們還可以使用集合工具類--Collections,通過調(diào)用其中的靜態(tài)方法,來得到線程安全的集合。具體方法,包括:Collections.synchronizedCollection(Collection<T> c)、Collections.synchronizedSet(Set<T> s)、Collections.synchronizedList(List<T>)、Collections.synchronizedMap(Map<K, V>)。
究其原理,他們都是通過在方法中加synchronized同步鎖來實(shí)現(xiàn)的。我們知道synchronized鎖的開銷較大,在程序中不建議使用。

雖然,這三種方式可以實(shí)現(xiàn)線程安全的集合,但是都有顯而易見的缺點(diǎn),而且也不是我們今天所關(guān)注的重點(diǎn)。

接下來,就來具體看下java.util.concurrent包中的實(shí)現(xiàn);

5.2 并發(fā)集合實(shí)現(xiàn)2

在java.util.concurrent包中,提供了兩種類型的并發(fā)集合:一種是阻塞式,另一種是非阻塞式。

阻塞式集合:當(dāng)集合已滿或?yàn)榭諘r,被調(diào)用的添加(滿)、移除(空)方法就不能立即被執(zhí)行,調(diào)用這個方法的線程將被阻塞,一直等到該方法可以被成功執(zhí)行。

非阻塞式集合:當(dāng)集合已滿或?yàn)榭諘r,被調(diào)用的添加(滿)、移除(空)方法就不能立即被執(zhí)行,調(diào)用這個方法的線程不會被阻塞,而是直接則返回null或拋出異常。

下面,就來看下concurrent包下,到底存在了哪些線程安全的集合:

Collection集合:

List:

CopyOnWriteArrayList

Set:

CopyOnWriteArraySet
ConcurrentSkipListSet

Queue:

BlockingQueue:
    LinkedBlockingQueue
    DelayQueue
    PriorityBlockingQueue
    ConcurrentLinkedQueue
    TransferQueue:
        LinkedTransferQueue
    BlockingDeque:
        LinkedBlockingDeque
        ConcurrentLinkedDeque

Map集合:

Map:

ConcurrentMap:
    ConcurrentHashMap
    ConcurrentSkipListMap
    ConcurrentNavigableMap

通過以上可以看出,java.util.concurrent包為每一類集合都提供了線程安全的實(shí)現(xiàn)。

接下來,我們做具體分析!

5.3 List并發(fā)集合(CopyOnWrite機(jī)制)

  1. CopyOnWrite機(jī)制

CopyOnWrite(簡稱COW),是計(jì)算機(jī)程序設(shè)計(jì)領(lǐng)域中的一種優(yōu)化策略,也是一種思想--即寫入時復(fù)制思想。

那么,什么是寫入時復(fù)制思想呢?就是當(dāng)有多個調(diào)用者同時去請求一個資源時(可以是內(nèi)存中的一個數(shù)據(jù)),當(dāng)其中一個調(diào)用者要對資源進(jìn)行修改,系統(tǒng)會copy一個副本給該調(diào)用者,讓其進(jìn)行修改;而其他調(diào)用者所擁有資源并不會由于該調(diào)用者對資源的改動而發(fā)生改變。這就是寫入時復(fù)制思想;

如果用代碼來描述的話,就是創(chuàng)建多個線程,在每個線程中如果修改共享變量,那么就將此變量進(jìn)行一次拷貝操作,每次的修改都是對副本進(jìn)行。

代碼如下:

public class CopyOnWriteThread implements Runnable {

    private List<String> list = new ArrayList<String>();

    public void run() {
        List<String> newList = new ArrayList<String>();
        newList.add("hello");
        Collections.copy(newList,list);
        
    }
   
    //創(chuàng)建線程:
    public static void main(String[] agrs){
        Thread thread1 = new Thread(new CopyOnWriteThread());
        thread1.start();

        Thread thread2 = new Thread(new CopyOnWriteThread());
        thread2.start();
    }
}

從JDK1.5開始,java.util.concurrent包中提供了兩個CopyOnWrite機(jī)制容器,分別為CopyOnWriteArrayList和CopyOnWriteArraySet。

CopyOnWriteArrayList,直白翻譯過來就是“當(dāng)寫入時復(fù)制ArrayList集合”。

簡單的理解,就是當(dāng)我們往CopyOnWrite容器中添加元素時,不直接操作當(dāng)前容器,而是先將容器進(jìn)行Copy,然后對Copy出的新容器進(jìn)行修改,修改后,再將原容器的引用指向新的容器,即完成了整個修改操作;

  1. CopyOnWriteArrayList的實(shí)現(xiàn)原理

CopyOnWriteArrayList,線程安全的集合,這一點(diǎn)主要區(qū)別與ArrayList。

通常來說,線程安全都是通過加鎖實(shí)現(xiàn)的,那么CopyOnWriteArrayList是如何實(shí)現(xiàn)?

CopyOnWriteArrayList通過使用ReentrantLock鎖來實(shí)現(xiàn)線程安全:

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    //ReentrantLock鎖,沒有使用Synchronized
    transient final ReentrantLock lock = new ReentrantLock();

    //集合底層數(shù)據(jù)結(jié)構(gòu):數(shù)組(volatile修飾共享可見)
    private volatile transient Object[] array;
}

CopyOnWriteArrayList在添加、獲取元素時,使用getArray()獲取底層數(shù)組對象,獲取此時集合中的數(shù)組對象;使用setArray()設(shè)置底層數(shù)組,將原有數(shù)組對象指針指向新的數(shù)組對象----實(shí)以此來實(shí)現(xiàn)CopyOnWrite副本概念:

//CopyOnWrite容器中重要方法:獲取底層數(shù)組。
final Object[] getArray() {
    return array;
}

//CopyOnWrite容器中重要方法:設(shè)置底層數(shù)組
final void setArray(Object[] a) {
    array = a;
}

CopyOnWriteArrayList添加元素:在添加元素之前進(jìn)行加鎖操作,保證數(shù)據(jù)的原子性。在添加過程中,進(jìn)行數(shù)組復(fù)制,修改操作,再將新生成的數(shù)組復(fù)制給集合中的array屬性。最后,釋放鎖;

由于array屬性被volatile修飾,所以當(dāng)添加完成后,其他線程就可以立刻查看到被修改的內(nèi)容。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //加鎖:
    lock.lock();
    try {
        //獲取集合中的數(shù)組:
        Object[] elements = getArray();
        int len = elements.length;
        
        //數(shù)組復(fù)制:將此線程與其他線程對集合的操作區(qū)分開來,無論底層結(jié)構(gòu)如何改變,本線程中的數(shù)據(jù)不受影響
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        
        //對新的數(shù)組進(jìn)行操作:
        newElements[len] = e;

        //將原有數(shù)組指針指向新的數(shù)組對象:
        setArray(newElements);
        return true;
    } finally {
        //釋放鎖:
        lock.unlock();
    }
}

CopyOnWriteArrayList獲取元素:在獲取元素時,由于array屬性被volatile修飾,所以每當(dāng)獲取線程執(zhí)行時,都會拿到最新的數(shù)據(jù)。此外,添加線程在進(jìn)行添加元素時,會將新的數(shù)組賦值給array屬性,所以在獲取線程中并不會因?yàn)樵氐奶砑佣鴮?dǎo)致本線程的執(zhí)行異常。因?yàn)楂@取線程中的array和被添加后的array指向了不同的內(nèi)存區(qū)域。

//根據(jù)角標(biāo),獲取對應(yīng)的數(shù)組元素:
public E get(int index) {
    return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

看到這,不知道你是不是跟我一樣,突然有個疑惑,在add()方法時已經(jīng)加了鎖,為什么還要進(jìn)行數(shù)組復(fù)制呢,難道不是多此一舉嗎?

其實(shí)不然,為了能讓get()方法得到最大的性能,CopyOnWriteArrayList并沒有進(jìn)行加鎖處理,而且也不需要加鎖處理。

因?yàn)?,在add()時候加了鎖,首先不會有多個線程同時進(jìn)到add中去,這一點(diǎn)保證了數(shù)組的安全。當(dāng)在一個線程執(zhí)行add時,又進(jìn)行了數(shù)組的復(fù)制操作,生成了一個新的數(shù)組對象,在add后又將新數(shù)組對象的指針指向了舊的數(shù)組對象指針,注意此時是指針的替換,原來舊的數(shù)組對象還存在。這樣就實(shí)現(xiàn)了,添加方法無論如何操作數(shù)組對象,獲取方法在獲取到集合后,都不會受到其他線程添加元素的影響。

這也就是在執(zhí)行add()時,為什么還要在加鎖的同時又copy了一分新的數(shù)組對象?。?!

模擬CopyOnWriteArrayList:

public class CopyOnWriteThread{

    private static CopyOnWriteTestList copyOnWriteTestList = new CopyOnWriteTestList();

    static class CopyOnWriteTestList{
        private Object[] array;

        public CopyOnWriteTestList(){
            this.array=new Object[0];
        }
        //獲取底層數(shù)組:
        public Object[] getArray(){
            return array;
        }
        //設(shè)置底層數(shù)組:
        public void setArray(Object[] array) {
            this.array = array;
        }

        //添加元素:
        public void add(String element){
            int len = array.length;
            Object[] newElements = Arrays.copyOf(array, len + 1);
            newElements[len] = element;
            setArray(newElements);
        }

        public void get(int index){
            Object[] array = getArray();
            get(array,index);
        }
        //此步驟,就是為了驗(yàn)證在獲取元素時,array是否會隨著元素的添加而改變;
        public void get(Object[] array,int index){
            for(;;){
                System.out.println("獲取方法:"+array.length);
            }
        }
    }
    //創(chuàng)建線程:
    public static void main(String[] agrs) throws InterruptedException {
        //啟動異步線程,一直添加元素
        new ThreadPoolExecutor(10,10,10, TimeUnit.MINUTES,
                new ArrayBlockingQueue(11),
                new ThreadPoolExecutor.AbortPolicy()).execute(new Runnable() {
            public void run() {
                for(;;){
                    int x=0;;
                    copyOnWriteTestList.add("jiaboyan"+x);
                    ++x;
                }
            }
        });
        Thread.sleep(1000);
        System.out.println(copyOnWriteTestList.getArray().length);
        //啟動線程:獲取元素
        new Runnable() {
            public void run() {
                copyOnWriteTestList.get(0);
            }
        }.run();
    }
}
  1. CopyOnWrite機(jī)制的優(yōu)缺點(diǎn)

CopyOnWriteArrayList保證了數(shù)據(jù)在多線程操作時的最終一致性。

缺點(diǎn)也同樣顯著,那就是內(nèi)存空間的浪費(fèi):因?yàn)樵趯懖僮鲿r,進(jìn)行數(shù)組復(fù)制,在內(nèi)存中產(chǎn)生了兩份相同的數(shù)組。如果數(shù)組對象比較大,那么就會造成頻繁的GC操作,進(jìn)而影響到系統(tǒng)的性能;

剛才說了,CopyOnWriteArrayList只能保證最終的數(shù)據(jù)一致性,而不能保證實(shí)時的數(shù)據(jù)一致性。這一點(diǎn)也是我們在使用的過程中,必須要考慮到的因素。

仔細(xì)思考下,其實(shí)CopyOnWrite容器也是一種讀寫分離,讀和寫是不同的容器。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 下面是我自己收集整理的Java線程相關(guān)的面試題,可以用它來好好準(zhǔn)備面試。 參考文檔:-《Java核心技術(shù) 卷一》-...
    阿呆變Geek閱讀 15,120評論 14 507
  • Java-Review-Note——4.多線程 標(biāo)簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,761評論 2 17
  • /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home...
    光劍書架上的書閱讀 4,177評論 2 8
  • 《打開慧眼》啟示我們:能讓自己一生出彩的資本不是權(quán)力、不是地位、不是金錢、不是人脈關(guān)系、也不是學(xué)識經(jīng)驗(yàn)……而是你自...
    聶仁博閱讀 700評論 0 0
  • 中考化學(xué)選擇題是必考題型,它涉及知識的方方面面,綜合性較強(qiáng)。 有些化學(xué)選擇題則對學(xué)生的能力提出了更高的要求,但是只...
    稻谷龍閱讀 386評論 0 0

友情鏈接更多精彩內(nèi)容