集合類不安全之ArrayList

1. 不安全的ArrayList

大家都知道ArrayList線程不安全,怎麼個不安全法呢?上代碼:

public class ContainerNotSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0;i<5;i++){
            new Thread(()->{
                list.add(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+"\t"+list);
            }).start();
        }
    }
}

//運行結果如下: 多個線程同時修改列表的元素,產生了併發修改異常
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at java.util.AbstractCollection.toString(AbstractCollection.java:461)
    at java.lang.String.valueOf(String.java:2994)
    at java.lang.StringBuilder.append(StringBuilder.java:131)
    at juc.ContainerNotSafeDemo.lambda$main$0(ContainerNotSafeDemo.java:26)
    at java.lang.Thread.run(Thread.java:748)

爲啥呢?看一下add()方法的源碼:

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

可以看到僅僅是在擴容和添加操作,並沒有任何的線程安全控制。所以在實際的高併發場景下,ArrayList的應用很有侷限。

2. 安全的解決方式

2.1 使用Vector解決

注意到,ArrayList的add方法並沒有任何保證線程安全的機制 ~ 所以不安全了。怎麼解決呢?首先想到的是加鎖,湊巧的是Vector已經爲我們提供了安全的add方法:

public class ContainerNotSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new Vector<>();//修改爲Vector對象
        for (int i = 0;i<3;i++){
            new Thread(()->{
                list.add(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+"\t"+list);
            }).start();
        }
    }
}

//結果如下:
Thread-0    [Thread-0]
Thread-2    [Thread-0, Thread-2]
Thread-1    [Thread-0, Thread-2, Thread-1]

這麼看來,在這種情況下多線程的添加操作是沒有任何問題的?那麼,這麼做真的可取嘛?嘗試查看Vectoradd()方法源碼:

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

可以看到:Vector使用了synchronized關鍵字來保證線程安全。可是爲了添加一個的操作,加了個重鎖,這樣做在多線程環境下會造成嚴重的資源浪費與性能損耗!!高併發情況下是萬萬不可取的。矛盾來了:ArrayList可以提升併發性,但是犧牲了線程安全性,而Vector恰恰與之相反。所以,我們在不同的場合下可以根據業務需求有所取捨。


2.4 使用Collections解決

一個神奇的工具類——Collections,看一下它的結構:

可以看到這個工具類提供了SynchronizedList、SynchronizedMap、SynchronizedSet等看名字就很安全的類,怎麼實現的囁?看Collections.synchronizedList(List list) 方法源碼:

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

我們在傳值的時候傳的是ArrayList的對象,ArrayList它又實現了RandomAccess,所以返回 new SynchronizedRandomAccessList<>(list, mutex)對象,但是:

    static class SynchronizedRandomAccessList<E>    
        extends SynchronizedList<E>
        implements RandomAccess {
        ...
        }
 //它又繼承了SynchronizedList,所以返回的還是SynchronizedList對象

SynchronizedList它的源碼怎麼寫:

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        ...
        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 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);}
        }
    ...
 }

可以看到,SynchronizedList 的實現裏,get, set, add 等操作都加了 mutex 對象鎖,再將操作委託給最初傳入的 list。mutex來自哪裏?

        SynchronizedCollection(Collection<E> c) {
            this.c = Objects.requireNonNull(c);
            mutex = this;    //mutex就是這個list本身咯~
        }
        


2.4 使用CopyOnWriteArrayList解決

還是下邊這段代碼,進行了一下簡單的修改:

public class ContainerNotSafeDemo {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0;i<3;i++){
            new Thread(()->{
                list.add(Thread.currentThread().getName());
                System.out.println(Thread.currentThread().getName()+"\t"+list);
            }).start();
        }
    }
}

使用到了new CopyOnWriteArrayList<>();,字面意思看來時:寫時複製集合

先來了解一下Copy-On-Write(寫時複製技術):通俗的講,寫時複製技術就是不同進程訪問同一資源的時候,只有在寫操作,纔會去複製一份新的數據,否則都是訪問同一個資源。

CopyOnWriteArrayList,是一個寫入時複製的容器,它是如何工作的呢?簡單來說,就是平時查詢的時候,都不需要加鎖,隨便訪問,只有在寫入/刪除的時候,纔會從原來的數據複製一個副本出來,然後修改這個副本,最後把原數據替換成當前的副本。修改操作的同時,讀操作不會被阻塞,而是繼續讀取舊的數據。這點要跟讀寫鎖區分一下。

那麼java裏面是如何實現的,看源碼:

    /**
     * Appends the specified element to the end of this list.【添加元素至列表末尾】
     * @param e element to be appended to this list          【e:新增的元素】
     * @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);  //拷貝一份舊容器,並且擴容+1
            newElements[len] = e;                  //填充元素
            setArray(newElements);                 //新的集合列表替換掉舊的
            return true;
        } finally {
            lock.unlock();                     
        }
    }

我們看到Java中JDK的源碼實現其實也是非常的簡單,往一個容器裏添加元素的時候,不直接往當前容器object[]添加,而是先將當前容器object[]進行cpoy,複製出來一個新的容器object[] newElements,然後往新的容器newElements裏面添加元素。添加完成之後再將原容器的引用指向新的容器setArray(newElements);。

優點:對於一些讀多寫少的數據,這種做法的確很不錯,對容器併發的讀不需要加鎖,因爲此時容器內不會添加任何新的元素。所以CopyOnWriteArrayList也是一種讀寫分離的思想,讀和寫操作的是不同的容器。

缺點:這種實現只保證數據的最終一致性,在副本未替換掉舊數據時,讀到的仍然是舊數據。如果對象比較大,頻繁地進行替換會消耗內存,從而引發頻繁的GC,此時,應考慮其他的容器,例如ConcurrentHashMap。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章