1. 程式人生 > >多執行緒(八): Vector與ArrayList

多執行緒(八): Vector與ArrayList

ArrayList不允許寫操作沒執行完就執行讀操作,正在讀的時候不允許去寫,必須寫完再讀,讀完再寫。

一:ArrayList不安全示例

使用ArrayList每次列印的集合數量可能會小於10000,而使用Vector每次都是10000

public class ListTest {
    public static void main(String[] args) throws InterruptedException {
        ThreadGroup group = new ThreadGroup("myGroup");
        List list = new ArrayList<>();
        // Vector list = new Vector<>();
MyRunnable runnable = new MyRunnable(list); for (int i = 0; i < 10000; i++) { new Thread(group, runnable).start(); } // 保證所有執行緒都執行完畢 while (group.activeCount() > 0) { Thread.sleep(10); } System.out.println(list.size()); } } class
MyRunnable implements Runnable {
private List list; public MyRunnable(List list) { this.list = list; } @Override public void run() { try { Thread.sleep((int)(Math.random() * 2)); } catch (InterruptedException e) { } Random random = new
Random(); list.add(Thread.currentThread().getName() + "\t" + random.nextInt(100)); } }

ArrayList原始碼

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
   transient Object[] elementData;
   private int size;

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

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 先賦值                 
        elementData[index] = element;
        // 再更新數量
        size++;
    }
}

通過原始碼可以看到ArrayList是通過一個Object[]陣列來實現的,ArrayList在新增一個元素的時候,至少需要兩步操作:1. 更新集合大小 2. 將元素新增到集合中, 即 elementData[size++] = e; (注意是size++, 而不是++size, 先使用值,然後再相加) add方法並沒做什麼同步加鎖的處理,假如有兩個執行緒,剛開始size=0; Thread-A執行緒將元素e新增到集合中,但並沒有完成自加的操作,此時size仍然為0,elementData[0] = e,然後CPU切換到Thread-1執行緒,新增e2元素,因Thread-0並沒有完成自加操作所以此時size仍然為0,Thread-1執行完所有的add操作,結果elementData[0] = e2; size=1,Thread-1執行完CPU切換到Thread-0,完成自增賦值操作,因Thread-1已經完成自層操作了,所以當時的size=1,Thread-0完成自增後size=2了,可以看到size為2,但是兩個執行緒都是對索引0的位置操作,最終集合的長度為1,狀態不一致。

elementData[size++] = e 並不是原子操作,實際上至少會被分解為4個原子操作

  1. 讀取size的值
  2. int newSize = size + 1;
  3. size = newSize;
  4. elementData[size] = e;

ArrayList是執行緒不安全的,可以通過Collections包裝成執行緒安全的集合

List<String> list = Collections.synchronizedList(new ArrayList<String>());

Collections: 集合工具類

package java.util;
public class Collections {
    // 將一些非執行緒安全的集合包裝成執行緒安全的集合
    public static <T> Collection<T> synchronizedCollection(Collection<T> c);
    public static <T> Set<T> synchronizedSet(Set<T> s);
    public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
    public static <T> List<T> synchronizedList(List<T> list){
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
    public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);


    // 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;
        }
     } 


    // SynchronizedCollection
    static class SynchronizedCollection<E> implements Collection<E>, Serializable {
        private static final long serialVersionUID = 3053995032091335093L;

        final Collection<E> c;  // Backing Collection
        // 同步鎖
        final Object mutex;     // Object on which to synchronize

        SynchronizedCollection(Collection<E> c) {
            this.c = Objects.requireNonNull(c);
            // 同步鎖為this,當前物件
            mutex = this;
        }

        public boolean add(E e) {
            // add 方法使用了同步鎖: this
            // 同一時間只能新增一個
            synchronized (mutex) {return c.add(e);}
        }

    }  
}

示例一

public class WriteThread extends Thread {
    private final List<Integer> list;
    public WriteThread(List<Integer> list) {
        super("WriteThread");
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; true; i++) {
            list.add(i);
        }
    }
}
public class ReaderThread extends Thread {
    private final List<Integer> list;
    public ReaderThread(List<Integer> list) {
        super("ReaderThread");
        this.list = list;
    }

    @Override
    public void run() {
        while (true) {
            for (int n : list) {
                System.out.println(n);
            }
        }
    }
}
public class Main {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        new WriteThread(list).start();
        new ReaderThread(list).start();
    }
}
Exception in thread "ReaderThread" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.company.example.chapter02.example2.ReaderThread.run(ReaderThread.java:25)

示例2

public static void main(String[] args) {
    // 包裝成同步集合
    final List<Integer> list = Collections.synchronizedList(new ArrayList<>());
    // SynchronizedCollection#add 已經加了同步鎖this,所以不會發生同步寫
    new WriteThread(list).start();
    // 雖然寫與寫不會同步,但是目前寫的同時可以讀,這樣是不行的,必須保證寫的時候不能讀,讀的時候不能寫
    // 所以在讀的時候也要加鎖,而且讀的時候加的鎖必須和寫的鎖一致
    // 如果能保證讀與寫互斥,寫與寫互斥就沒有執行緒安全問題
    // 注意:雖然Collections.synchronizedList是執行緒安全的,但是並不是說你的程式也安全,同時也要保證自己的邏輯也要安全
    new ReaderThread(list).start();
}

public class ReaderThread extends Thread {
    // ...

    @Override
    public void run() {
        while (true) {
            // 讀寫互斥
            synchronized (list) {
                for (int n : list) {
                    System.out.println(n);
                }
            }
        }
    }
}

示例三
CopyOnWriteArrayList是執行緒安全的,copy-on-write:寫時複製,當對集合執行寫操作是,內部已確保安全的陣列就會被整體複製,複製之後,就無需在使用迭代器依次讀取元素時擔心元素被修改了,所以在讀時也不會發生java.util.ConcurrentModificationException

public static void main(String[] args) {
    // WriteThread 和 ReaderThread 和示例一完全一樣
    // 只需要使用CopyOnWriteArrayList
    final List<Integer> list = new CopyOnWriteArrayList<>();

    new WriteThread(list).start();
    new ReaderThread(list).start();
}

CopyOnWriteArrayList

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    // 鎖
    final transient ReentrantLock lock = new ReentrantLock();
    // 陣列
    private transient volatile Object[] array;


    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;
            // 將新陣列整體複製給array
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }


    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }


    // get無需加鎖
    public E get(int index) {
        return get(getArray(), index);
    }   
}    

使用copy-on-write時,每次執行寫操作都會執行復制。因此頻繁執行寫操作時,如果使用CopyOnWriteArrayList會比較花費時間,如果寫操作較少讀操作較多就非常適用該集合。


Vector

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    protected Object[] elementData;
    protected int elementCount;

    // 執行緒安全,使用synchronized
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
   }
}

將上面的list宣告為Vector可以看到每次執行集合的長度都是10000

三:Vector與ArrayList比較

Vector與ArrayList主要在於執行緒安全和效能上的區別。

  • Vector與ArrayList本質上都是一個Object[] 陣列,ArrayList提供了size屬性,Vector提供了elementCount屬性,他們的作用是記錄集合內有效元素的個數。
  • Vector是執行緒安全的集合類,ArrayList並不是執行緒安全的類。Vector類對集合的元素操作時都加了synchronized,保證執行緒安全。
  • Vector與ArrayList的擴容並不一樣,Vector預設擴容是增長一倍的容量,Arraylist是增長50%的容量。
  • Vector與ArrayList的remove,add(index,obj)方法都會導致內部陣列進行資料拷貝的操作,這樣在大資料量時,可能會影響效率。