java高并發(fā)下CopyOnWriteArrayList替代ArrayList
一、ArrayList線程不安全
在Java的集合框架中,想必大家對ArrayList肯定不陌生,單線程的情況下使用它去做一些CRUD的操作是非常方便的,先來看看這個例子:
public class ListTest { public static void main(String[] args) { List<String> arrayList = new ArrayList<>(); arrayList.add("a"); arrayList.add("b"); arrayList.add("c"); for (String s : arrayList) { System.out.println(s); } } }
其輸出結果就是與元素被添加進ArrayList的順序一樣,即:
a
b
c
但是到了多線程的情況下,ArrayList還會像單線程一樣執(zhí)行結果符合我們的預期嗎?我們再來看下這個例子:
public class ListTest { public static void main(String[] args) { List<String> arrayList = new ArrayList<>(); for (int i = 0; i < 30; i++) { new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一個長為5的隨機字符串 System.out.println(arrayList); //讀取list },"線程"+(i+1)).start(); } } }
由輸出結果:
Exception in thread "線程10"
Exception in thread "線程1"
Exception in thread "線程14"
Exception in thread "線程3"
Exception in thread "線程5"
Exception in thread "線程2"
Exception in thread "線程6"
Exception in thread "線程21"
Exception in thread "線程23"
Exception in thread "線程28"
Exception in thread "線程29"
java.util.ConcurrentModificationException
我們發(fā)現,多線程的情況下,有多個線程對ArrayList添加元素,同時又會有多個元素對ArrayList進行元素的讀取,這樣使得程序拋出了ConcurrentModificationException
并發(fā)修改異常,所以我們可以下定結論:ArrayList線程不安全!
二、解決ArrayList線程不安全的方案
1、使用Vector類
我們知道,Java集合中的Vector類是線程安全的,可以用Vector去解決上述的問題。
public class ListTest { public static void main(String[] args) { List<String> arrayList = new Vector<>(); for (int i = 0; i < 30; i++) { new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一個長為5的隨機字符串 System.out.println(arrayList); //讀取list },"線程"+(i+1)).start(); } } }
翻看Vector的源碼可知,其add
方法是使用了synchronized
同步鎖,同一時刻只允許一個線程對List進行修改。雖然Vector能夠保證線程安全,但通過前面幾期推文的學習,synchronized
的方案一般不是最優(yōu)選擇,會對程序的性能有一定的影響。
2、使用Collections類
Collections類中的synchronizedList
方法可以使一個線程不安全的List轉為線程安全的。
public class ListTest { public static void main(String[] args) { List<String> arrayList = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 30; i++) { new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一個長為5的隨機字符串 System.out.println(arrayList); //讀取list },"線程"+(i+1)).start(); } } }
即使是不看synchronizedList
的源碼我們也可以通過它的名字猜到其底層也是使用synchronized
來保證線程安全的,這也不是最優(yōu)解。
3、使用CopyOnWriteArrayList類
CopyOnWriteArrayList類就是我們今天要主要探討的重頭。
public class ListTest { public static void main(String[] args) { List<String> arrayList = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 30; i++) { new Thread(()->{ arrayList.add(UUID.randomUUID().toString().substring(0,5)); //往list中添加一個長為5的隨機字符串 System.out.println(arrayList); //讀取list },"線程"+(i+1)).start(); } } }
下面我們來聊聊CopyOnWriteArrayList是這么實現線程安全的。
三、CopyOnWriteArrayList
1、簡介
java.util.concurrent
包下的并發(fā)List只有CopyOnWriteArrayList。CopyOnWriteArrayList是一個線程安全的ArrrayList,對其進行的修改操作都是在底層的一個復制的數組上進行的,采用了寫時復制,讀寫分離的思想。其類圖結構如下:
通過類圖可以清楚下面幾點:
- 每個CopyOnWriteArrayList對象中有一個
array
數組對象用來存放具體元素 - ReentrantLock獨占鎖對象用來保證同一時刻只有一個線程對array進行修改
如果要我們自己來實現一個寫時復制的線程安全的List,要考慮哪些點呢?
下面我們帶著以下的問題與思考,來學習下CopyOnWriteArrayList吧!
- 何時初試化list,初始化的list元素個數為多少,list的大小是是有限的嗎?
- 如何保證線程安全,多個線程對list進行讀寫時是如何保證線程安全的?
- 如何確保使用迭代器變量list時的數據一致性?
2、主要方法源碼分析
1、初始化
無參構造函數,其實是在內部創(chuàng)建了一個大小為0的Object數組作為array的初始值
public CopyOnWriteArrayList() { setArray(new Object[0]); }
有參構造函數有兩個:
public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
CopyOnWriteArrayList中的array數組元素是入參toCopyIn數組元素的拷貝。
public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } setArray(elements); }
入參為集合時,將集合里的元素復制到CopyOnWriteArrayList中。
2、添加元素
CopyOnWriteArrayList中用來添加元素的方法有很多,原理均類似,故選取add(E e)
方法來進行學習。
public boolean add(E e) { //(1) final ReentrantLock lock = this.lock; lock.lock(); try { //(2) Object[] elements = getArray(); //(3) int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; //(4) setArray(newElements); return true; } finally { //(5) lock.unlock(); } }
上述代碼中,獨占鎖的思想非常值得學習。
- 調用add方法的線程會首先執(zhí)行代碼(1)去獲取獨占鎖,如果有多個線程都調用add方法時則只有一個線程會去獲取到該鎖,其他線程會被阻塞掛起直到鎖被釋放。
- 一個線程獲取到鎖后,就保證了該線程添加元素的過程中其他線程不會對array數組進行修改。
- 線程獲取到所后執(zhí)行代碼(2)獲取array,然后執(zhí)行代碼(3)復制array到一個新數組中,新數組的大小是array數組的大小+1,所以CopyOnWriteArrayList是無界的List,并把新的元素添加進新數組中。
- 代碼(4)使用新數組替換原數組,并在返回前執(zhí)行(5)釋放鎖。由于加了鎖,所以整個add的過程是原子性操作。
小結一下就是,添加元素時,線程先獲取獨占鎖,然后復制原數組到新數組,給新數組添加元素,再把添加完元素后的新數組復制回原數組,最后釋放鎖返回。這就是所謂的寫時復制。
3、獲取指定位置元素
使用 E get(int index)
獲取下班為index的元素,如果元素不存在則拋出IndexOutOfBoundsException
異常。
public E get(int index) { return get(getArray(), index); } final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
上述的代碼中,當線程x調用get方法獲取指定位置的元素時,需要分兩步,首先獲取array數組,然后通過下標訪問指定位置的元素,這個過程是沒有加鎖同步的。假設這時候List的內容如圖所示,里面有1、2、3的元素:
由于線程x調用get方法時是沒有加鎖的,這就可能導致線程x在獲取完array數組之后、訪問指定位置元素之前,另外一個線程y進行了remove操作,假設要刪除元素1,remove操作首先或獲取獨占鎖,然后進行寫時復制,也就是復制一份當前array數組,然后在復制的數組里刪除線程x要調用get方法訪問的元素1,然后讓array指向復制的數組。所以這個時候array之前指向的數組的引用計數為1而不為0,因為線程x還在使用它,這是線程x要訪問指定位置的元素了,而它操作的數組就是線程B刪除元素之前的數組。如下示意圖:
雖然線程y刪除了index處的元素,但是線程x還是能夠讀到index處的元素,這就是寫時復制策略產生的弱一致性問題。
4、修改指定元素
使用E set(int index, E element)
方法修改list中指定元素的值時,如果指定位置元素不存在則拋出IndexOutOfBoundsException
異常。
public E set(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); E oldValue = get(elements, index); if (oldValue != element) { int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics setArray(elements); } return oldValue; } finally { lock.unlock(); } }
上述代碼也是先獲取獨占鎖,從而阻止其他線程對array數組進行修改,然后獲取當前數組,調用get方法獲取指定位置的元素,若指定位置的元素值與新值不一致,則創(chuàng)建新數組并復制元素,然后在新數組上修改元素值,再將array指向新數組;如果指定位置的元素值與新值一直,則為了保證volatile語義,還是要重新設置array,雖然其值為改變。
5、刪除元素
這里介紹E remove(int index)
方法。
public E remove(int index) { //獲取獨占鎖 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //獲取指定元素 E oldValue = get(elements, index); int numMoved = len - index - 1; //如果要刪除的是最后一個元素 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { //刪除的不是最后一個元素,則分兩次復制刪除后剩余的元素到新數組中 Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); //使用新數組替換原數組 setArray(newElements); } return oldValue; } finally { //釋放鎖 lock.unlock(); } }
如上代碼其實和新增元素的代碼類似,首先獲取獨占鎖以保證刪除數據期間其他線程不能對 array 進行修改,然后獲取數組中要被刪除的元素,并把剩余的元素復制到新數組,之后使用新數組替換原來的數組,最后在返回前釋放鎖。
6、弱一致性的迭代器
在講解什么是迭代器的弱一致性前,先看看下面的例子:
public class ListTest { public static void main(String[] args) { List<String> arrayList =new CopyOnWriteArrayList<>(); arrayList.add("Hello"); arrayList.add("HuYa"); Iterator<String> iterator = arrayList.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } } }
輸出如下:
Hello
HuYa
iterator的hasNext
方法用于判斷集合中是否還有元素,next
用于返回具體元素。 那么CopyOnWriteArrayList中迭代器的弱一致性又是啥意思?所謂弱一致性,是指返回迭代器后,其他線程對list的增刪改對迭代器是不可見的。
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { //array的快照版本 private final Object[] snapshot; //數組下標 private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } }
上述代碼中,當CopyOnWriteArrayList對象調用iterator()方法獲取迭代器時實際上會返回COWIterator對象,COWIterator對象的snapshot變量保存了當前l(fā)ist的內容, cursor是遍歷list時數據的下標。
特別對snapshot快照進行一個說明:
- 如果在一個線程使用list返回的迭代器遍歷元素的過程中,其他線程沒有對list進行修改,那么snapshot本身就是list的array了。
- 如果在一個線程使用list返回的迭代器遍歷元素的過程中,其他線程有對list進行修改,那么snapshot就是array的一個快照了,因為修改后list里面的數組其實是被新數組替換了,這時候原來的數組是被snapshot繼續(xù)引用。
這也說明一個線程獲取了迭代器后,使用該迭代器元素時,其他線程對該list進行的修改是不可見的,因為它們操作的是兩個不同的數組,這也就是弱一致性。
最后再來看看一個例子:
public class ListTest { public static void main(String[] args) throws InterruptedException { List<String> arrayList =new CopyOnWriteArrayList<>( ); arrayList.add("Hello"); arrayList.add("HuYa"); arrayList.add("Welcome"); arrayList.add("to"); arrayList.add("Guangzhou"); Thread thread = new Thread(() -> { arrayList.set(1, "WeiXin"); arrayList.remove(2); arrayList.remove(3); }); //保證在修改線程啟動前先獲取迭代器 Iterator<String> iterator = arrayList.iterator(); Thread.sleep(1000); //修改線程啟動 thread.start(); //等待修改執(zhí)行完畢 thread.join(); //迭代元素 while (iterator.hasNext()){ System.out.println(iterator.next()); } } }
輸出結果如下:
Hello
HuYa
Welcome
to
Guangzhou
雖然說子線程對arrayList進行了修改,但是主線程在arrayList被修改之前先獲取到了其迭代器,拿到了原來數組的快照,所以子線程的修改對于主線程使用迭代器進行迭代并沒有影響。這就體現了CopyOnWriteArrayList
迭代器的弱一致性。
四、總結
本期主要學習了CopyOnWriteArrayList一些主要方法的源碼、思想,總結一下:
- 最主要的是它寫時復制的策略,來保證List的一致性,獲取、修改、寫入三步操作不是原子性的,故在增刪改的過程中都使用了獨占鎖,來保證同一時刻只能有一個線程對list進行修改。
- CopyOnWriteArrayList提供了弱一致性的迭代器,從而保證在獲取迭代器后,其他線程對list的修改對于迭代器是不可見的。
- 綜合CopyOnWriteArrayList上述的特性,它適用于讀多寫少的高并發(fā)場景。
但是CopyOnWriteArrayList也有缺點,開發(fā)時要注意一下:
- 內存占用問題。因為CopyOnWriteArrayList的寫時復制機制,所以在進行寫操作的時候,內存里會同時駐扎兩個對象的內存,即舊的對象和新寫入的對象(注意:在復制的時候只是復制容器里的引用,只是在寫的時候會創(chuàng)建新對象添加到新容器里,而舊容器的對象還在使用,所以有兩份對象內存)。如果這些對象占用的內存比較大,這個時候很有可能造成頻繁的GC,應用響應時間也隨之變長。
- 數據一致性問題。CopyOnWriteArrayList容器只能保證數據的最終一致性,不能保證數據的實時一致性。
以上就是java高并發(fā)下CopyOnWriteArrayList替代ArrayList的詳細內容,更多關于java高并發(fā)CopyOnWriteArrayList的資料請關注腳本之家其它相關文章!
相關文章
IDEA?Error:java:無效的源發(fā)行版:13的解決過程
之前用idea運行時,也會出現這種情況,后面通過網上的資料解決了這個問題,下面這篇文章主要給大家介紹了關于IDEA?Error:java:無效的源發(fā)行版:13的解決過程,需要的朋友可以參考下2023-01-01使用SpringBoot+Prometheus+Grafana實現可視化監(jiān)控
本文主要給大家介紹了如何使用Spring?actuator+監(jiān)控組件prometheus+數據可視化組件grafana來實現對Spring?Boot應用的可視化監(jiān)控,文中有詳細的代碼供大家參考,具有一定的參考價值,需要的朋友可以參考下2024-02-02