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]
這麼看來,在這種情況下多線程的添加操作是沒有任何問題的?那麼,這麼做真的可取嘛?嘗試查看Vector
的add()
方法源碼:
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。