程序猿學社的GitHub,歡迎Star
https://github.com/ITfqyd/cxyxs
本文已記錄到github,形成對應專題。
文章目錄
前言:
在學習多線程的過程中,我們經常會聽到ArrayList線程不安全。有個別社友就在說,我們在項目中用的好好的,也沒有什麼報錯,實際上,大部分的人,在項目開發過程中,確實很少接觸到這塊。來我們一探究竟唄。看看爲什麼說ArrayList線程不安全。
1.淺談ArrayList
一文了解ArrayList源碼及擴容
在多線程環境下,操作同一資源,會有各種各樣的問題出現,例如,在多線程環境下,操作ArrayList進行添加操作,就會出現java.util.ConcurrentModificationException的異常。我們常說某某是不是線程安全,實際上,是有一個前提的,是在多線程環境裏,因爲單線程過程中不會存在線程不安全的問題。
package com.cxyxs.thread.twelve;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 20:00
* Modified By:
*/
public class Demo1 {
public static void main(String[] args){
List<Integer> lists = new ArrayList<>();
for (int i = 0; i < 50; i++) {
new Thread(()->{
for (int j = 0; j < 50; j++) {
lists.add(j);
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
while (true){
if (Thread.activeCount() == 2){
System.out.println(lists.size());
return;
}
}
//Thread.currentThread().getThreadGroup().list();
}
}
- TimeUnit.MILLISECONDS.sleep(1) 線程休眠一毫秒,不要使用 Thread.sleep(1),這兩句代碼都是一樣的意思,我們應該儘量使用juc下的內容。
- 50個線程,打印50次,每次休眠1毫秒,鬼知道什麼時候能打完,看到社長之前的文章的社友,應該都知道,啓動main方法後,會有兩個線程一直處於運行狀態,所以,我們只需要判斷正在運行的線程數是不是爲2,就可以知道子線程的業務邏輯是否已經跑完,因爲run執行完後,線程會進入死亡狀態。
讀過小學的,都知道50*50,是2500,爲什麼打印的結果是2463?
- 這就是爲什麼說ArrayList是線程不安全的原因。
話不多說,上ArrayList的add源碼
private int size;
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
size++就是線程不安全的,需要大家瞭解JMM的可見性,不瞭解可以大致看看我之前的文章。
【多線程併發編程】六 什麼是線程安全?
- 假設size爲10,主內存會存size=10,其他的線程,會copy一份,放到各自的工作內存裏面,線程A,發現size爲10,所以+1後,size就會11了,注意,這時候線程B,發現size也是10,也執行+1操作,size變成11,線程A和線程B會把操作的結果通知給主內存,所以主內存的值變成11,實際上,應該爲12纔對。
2.解決線程不安全問題
使用Vector
package com.cxyxs.thread.twelve;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 20:00
* Modified By:
*/
public class Demo1 {
public static void main(String[] args){
//線程不安全
//List<Integer> lists = new ArrayList<>();
// 優化方案1
List<Integer> lists = new Vector<>();
for (int i = 0; i < 50; i++) {
new Thread(()->{
for (int j = 0; j < 50; j++) {
lists.add(j);
try {
Thread.sleep(1);
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
while (true){
if (Thread.activeCount() == 2){
System.out.println(lists.size());
return;
}
}
//Thread.currentThread().getThreadGroup().list();
}
}
- 社長運行了幾次,每次的結果都是2500,說明線程不安全的問題,已經解決。
我們來看一看Vector源碼,來看看他是怎麼解決線程不安全問題的。
使用ctrl+F12(idea),輸入add快速定位到這個方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
- synchronized 關鍵字,上鎖,意思同一時刻只能有一個線程,能操作add方法。類似於,有一個茅坑,裏面有幾個坑,有一個怪癖,每次上茅坑,都把大門都關起來。造成資源的浪費,其他的人想上茅坑,只能等隔壁老王上完才能使用。所以這種方式,不怎麼建議使用,根據場景,自己合理選擇。
- modCount看起來很陌生,他表示結構被修改的次數。新增,修改等modCount都會增加1
多線程開發經常遇到的報錯ConcurrentModificationException
之前在開發過程中,經常發現有ConcurrentModificationException這個報錯。
看名字其義,意思就是多線程修改異常,也就說多個線程對同一份資源進行寫操作會有這種異常報錯。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
我們看一看哪裏調用了這個方法
public E next() {
synchronized (Vector.this) {
checkForComodification();
int i = cursor;
if (i >= elementCount)
throw new NoSuchElementException();
cursor = i + 1;
return elementData(lastRet = i);
}
}
- 發現迭代器next方法會檢查修改的次數。調用Itr時,會把modCount賦值給expectedModCount。因爲modCount涉及到修改刪除等操作時,就會變動。
public synchronized Iterator<E> iterator() {
return new Itr();
}
- list的循環輸出iterator方法,就是上就是new Itr()
疑問
本人在調用add時,發現modCount每次調用add就會+1,但是expectedModCount一直沒有變,就有點困惑。個人感覺應該跟這些類的add方法應該有關係。對這方面瞭解很深的大佬,可以在下方留言。
使用Collections集合類
//優化方案二:
List<Integer> lists =Collections.synchronizedList(new ArrayList<>());
- 經過多次測試,結果都是2500.
- 有了synchronized,爲什麼還用弄出一個Collections,就是爲了方便我們開發,使我們更關注於業務邏輯的開發。
CopyOnWriteArrayList
package com.cxyxs.thread.twelve;
import java.lang.reflect.Array;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/**
* Description:轉發請註明來源 程序猿學社 - https://ithub.blog.csdn.net/
* Author: 程序猿學社
* Date: 2020/2/20 20:00
* Modified By:
*/
public class Demo1 {
public static void main(String[] args){
//線程不安全
//List<Integer> lists = new ArrayList<>();
// 優化方案1
//List<Integer> lists = new Vector<>();
//優化方案二:
//List<Integer> lists =Collections.synchronizedList(new ArrayList<>());
//優化方案三
List<Integer> lists = new CopyOnWriteArrayList();
for (int i = 0; i < 50; i++) {
new Thread(()->{
for (int j = 0; j < 50; j++) {
lists.add(j);
try {
Thread.sleep(1);
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
}
while (true){
if (Thread.activeCount() == 2){
System.out.println(lists.size());
return;
}
}
//Thread.currentThread().getThreadGroup().list();
}
}
通過多次測試,發現結果爲2500.
- CopyOnWriteArrayList是一個一個線程安全ArrayList,其中所有可變操作( add , set ,等等)通過對底層數組的最新副本實現
瞭解CopyOnWriteArrayList,我們首先需要先了解copyonwrite機制.
寫入時複製(CopyOnWrite,簡稱COW)思想是計算機程序設計領域中的一種優化策略。其核心思想是,如果有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。優點是如果調用者沒有修改該資源,就不會有副本(private copy)被建立,因此多個調用者只是讀取操作時可以共享同一份資源。
我們通過源碼來看一看,jdk大佬是如何實現CopyOnWriteArrayList的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);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
- 看到我們熟悉的人"lock",爲了避免死鎖,jdk官方的代碼就是跟try finally配套使用的,也是爲了try裏面的代碼報錯,鎖沒有釋放。
- 這簡單很簡潔,上鎖,獲取原來數組內容,再在原來數組長度上增加1,產生一個新的數組,並給最後一個元素複製。最後,再set進去。
- 看了這個代碼後,如果多個線程在操作過程中,我們讀,會讀到舊數據,這也是CopyOnWrite,寫入時複製,實際上換一個說法,就是讀寫分離。
我們看了CopyOnWriteArrayList的add方法,我們來看一看,他的iterator方法
public ListIterator<E> listIterator() {
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
/** Snapshot of the array */
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
@SuppressWarnings("unchecked")
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
- 不知道大家注意到沒有,我們可以發現調用iterator,實際上都用的是另外一個類,實際上就是把外面的object,複製給裏面這個類的數組。所以,個人認爲,讀到的也是舊的數據(多線程環境下)。
- 看了ArrayList,Vetoct,CopyOnWriteArrayList等等一些源碼,發現他們都是沒有直接迭代輸出的方法,都是藉助一個類實現ListIterator接口,大致都是這種套路。
總結:CopyOnWrite適用於讀多寫少的場景。因爲我們知道每次調用寫操作,都會重新開闢一個數組。對頻繁的寫操作,性能十分的底下。