多執行緒(八): 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個原子操作
- 讀取size的值
- int newSize = size + 1;
- size = newSize;
- 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)方法都會導致內部陣列進行資料拷貝的操作,這樣在大資料量時,可能會影響效率。