幾種Java同步List對比

CopyOnWrite容器

基本思想

Copy-On-Write,寫入時復(fù)制,這個技術(shù),準確的說應(yīng)該是一種思想,在很多系統(tǒng)設(shè)計上都會用到,今天我們來談一談Java語言中,JDK運用這種寫入時復(fù)制的思想的數(shù)據(jù)結(jié)構(gòu)/容器,CopyOnWriteArrayList。

CopyOnWriteArrayList,是一個寫入時復(fù)制的容器,它是如何工作的呢?簡單來說,就是平時查詢的時候,都不需要加鎖,隨便訪問,只有在寫入/刪除的時候,才會從原來的數(shù)據(jù)復(fù)制一個副本出來,然后修改這個副本,最后把原數(shù)據(jù)替換成當(dāng)前的副本。修改操作的同時,讀操作不會被阻塞,而是繼續(xù)讀取舊的數(shù)據(jù)。這點要跟讀寫鎖區(qū)分一下。

我們來看看CopyOnWriteArrayList的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();
    }
}
  • 首先可以看到CopyOnWriteArrayList中用到了ReentrantLock進行加鎖,
  • 添加元素時,保證同時只有1個線程進行變更,在變更的時候,先拷貝出來一個副本,先操作這個副本,操作完成后,再把現(xiàn)有的數(shù)據(jù)替換成這個副本。

再來看一下CopyOnWriteArraySet源碼

    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // Optimize for lost race to another addXXX operation
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

可以看到,同樣使用了復(fù)制的數(shù)據(jù)進行數(shù)據(jù)的添加

優(yōu)點

對于一些讀多寫少的數(shù)據(jù),這種做法的確很不錯,例如配置、黑名單、物流地址等變化非常少的數(shù)據(jù),這是一種無鎖的實現(xiàn)??梢詭臀覀儗崿F(xiàn)程序更高的并發(fā)。

缺點

這種實現(xiàn)只是保證數(shù)據(jù)的最終一致性,在添加到拷貝數(shù)據(jù)而還沒進行替換的時候,讀到的仍然是舊數(shù)據(jù)。如果對象比較大,頻繁地進行替換會消耗內(nèi)存,從而引發(fā)Java的GC問題,這個時候,我們應(yīng)該考慮其他的容器,例如ConcurrentHashMap。

Collections.synchronizedList

首先來看下Collections.synchronizedList的源碼:

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

這個方法回根據(jù)你傳入的List是否實現(xiàn)RandomAccess這個接口來返回的SynchronizedRandomAccessList還是SynchronizedList.

再看一下SynchronizedList的源碼

static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
    SynchronizedList(List<E> list, Object mutex) {
        super(list, mutex);
        this.list = list;
    }

    public boolean equals(Object o) {
        if (this == o)
            return true;
        synchronized (mutex) {return list.equals(o);}
    }
    public int hashCode() {
        synchronized (mutex) {return list.hashCode();}
    }

    public E get(int index) {
        synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }

    public int indexOf(Object o) {
        synchronized (mutex) {return list.indexOf(o);}
    }
    public int lastIndexOf(Object o) {
        synchronized (mutex) {return list.lastIndexOf(o);}
    }

    public boolean addAll(int index, Collection<? extends E> c) {
        synchronized (mutex) {return list.addAll(index, c);}
    }

    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }

    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }

    public List<E> subList(int fromIndex, int toIndex) {
        synchronized (mutex) {
            return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                        mutex);
        }
    }

    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        synchronized (mutex) {list.replaceAll(operator);}
    }
    @Override
    public void sort(Comparator<? super E> c) {
        synchronized (mutex) {list.sort(c);}
    }
    ... ...
}

可以看到SynchronizedList類中對add,remove, get等方法都加了synchronized關(guān)鍵字修飾,在保證List相關(guān)機制不變的情況下,保證的線程安全

但是可以看到 listIterator()獲取迭代器的相關(guān)方法并有使用synchronized關(guān)鍵字,因此在進行迭代遍歷的時候,需要加鎖

List<String> list = Collections.synchronizedList(new ArrayList<String>());
list.add("1");
list.add("2");
list.add("3");

synchronized (list) {
    Iterator i = list.iterator(); // Must be in synchronized block
    while (i.hasNext()) {
        //foo(i.next());
        System.out.println(i.next());
    }
}

幾種同步List實現(xiàn)對比

ArrayList

ArrayList是非線性安全,此類的 iterator 和 listIterator 方法返回的迭代器是快速失敗的:在創(chuàng)建迭代器之后,除非通過迭代器自身的 remove 或 add 方法從結(jié)構(gòu)上對列表進行修改,否則在任何時間以任何方式對列表進行修改,迭代器都會拋出 ConcurrentModificationException。即在一方在便利列表,而另一方在修改列表時,會報ConcurrentModificationException錯誤。而這不是唯一的并發(fā)時容易發(fā)生的錯誤,在多線程進行插入操作時,由于沒有進行同步操作,容易丟失數(shù)據(jù)。

因此,在開發(fā)過程當(dāng)中,ArrayList并不適用于多線程的操作。

Vector

從JDK1.0開始,Vector便存在JDK中,Vector是一個線程安全的列表,采用數(shù)組實現(xiàn)。其線程安全的實現(xiàn)方式是對所有操作都加上了synchronized關(guān)鍵字,這種方式嚴重影響效率,因此,不再推薦使用Vector了,Stackoverflow當(dāng)中有這樣的描述:Why is Java Vector class considered obsolete or deprecated?。

Collections.synchronizedList

Collections.synchronizedList的源碼可知,其實現(xiàn)線程安全的方式是建立了list的包裝類,代碼如下:

public static <T> List<T> synchronizedList(List<T> list) {  
return (list instanceof RandomAccess ?  
               new SynchronizedRandomAccessList<T>(list) :  
               new SynchronizedList<T>(list));//根據(jù)不同的list類型最終實現(xiàn)不同的包裝類。  
   }  

其中,SynchronizedList對部分操作加上了synchronized關(guān)鍵字以保證線程安全。部分SynchronizedList的代碼如下:

image.png

CopyOnWriteArrayList

從字面可以知道,CopyOnWriteArrayList在線程對其進行些操作的時候,會拷貝一個新的數(shù)組以存放新的字段。

CopyOnWriteArrayList和Collections.synchronizedList對比

CopyOnWriteArrayList和Collections.synchronizedList是實現(xiàn)線程安全的列表的兩種方式。兩種實現(xiàn)方式分別針對不同情況有不同的性能表現(xiàn),其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因為是采用了synchronized關(guān)鍵字的方式,其讀操作性能并不如CopyOnWriteArrayList。因此在不同的應(yīng)用場景下,應(yīng)該選擇不同的多線程安全實現(xiàn)類。


參考:

Collections.synchronizedList使用方法

CopyOnWriteArrayList與Collections.synchronizedList的性能對比

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

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