# 全網最細 | 21張圖帶你領略集合的執行緒不安全
阿新 • • 發佈:2020-08-31
# 全網最細 | 21張圖帶你領略集合的執行緒不安全
![封面圖](http://cdn.jayh.club/blog/20200830/z8NyApthH8zj.png?imageslim)
**本篇主要內容如下:**
![本篇主要內容](http://cdn.jayh.club/blog/20200829/Ho34aEb5noUs.png?imageslim)
本篇所有`示例程式碼`已更新到 [我的Github](https://github.com/Jackson0714/PassJava-Learning)
本篇文章已收納到我的[Java線上文件](www.jayh.club)
**《Java併發必知必會》系列:**
[1.反制面試官 | 14張原理圖 | 再也不怕被問 volatile!](https://www.cnblogs.com/jackson0714/p/java_volatile.html)
[2.程式設計師深夜慘遭老婆鄙視,原因竟是CAS原理太簡單?](https://www.cnblogs.com/jackson0714/p/CAS.html)
[3.用積木講解ABA原理 | 老婆居然又聽懂了!](https://www.cnblogs.com/jackson0714/p/ABA.html)
[4.全網最細 | 21張圖帶你領略集合的執行緒不安全](https://www.cnblogs.com/jackson0714/p/thread_safe_collections.html)
![集合,準備團戰](http://cdn.jayh.club/blog/20200828/U30tj9w8Yybo.gif)
## 一、執行緒不安全之ArrayList
**集合框架**有Map和Collection兩大類,Collection下面有List、Set、Queue。List下面有ArrayList、Vector、LinkedList。如下圖所示:
![集合框架思維導圖](http://cdn.jayh.club/blog/20200828/162802050.png)
**JUC併發包**下的集合類Collections有Queue、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentMap
![JUC包下的Collections](http://cdn.jayh.club/blog/20200828/162129623.png)
我們先來看看ArrayList。
### 1.1、ArrayList的底層初始化操作
首先我們來複習下ArrayList的使用,下面是初始化一個ArrayList,陣列存放的是Integer型別的值。
``` java
new ArrayList();
```
那麼底層做了什麼操作呢?
### 1.2、ArrayList的底層原理
#### 1.2.1 初始化陣列
```java
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
```
建立了一個空陣列,容量為0,根據官方的英文註釋,這裡容量應該為10,但其實是0,後續會講到為什麼不是10。
### 1.2.1 ArrayList的add操作
```java
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
```
重點是這一步:elementData[size++] = e; size++和elementData[xx]=e,這兩個操作**都不是**`原子操作`(不可分割的一個或一系列操作,要麼都成功執行,要麼都不執行)。
#### 1.2.2 ArrayList擴容原始碼解析
(1)執行add操作時,會先確認是否超過陣列大小
```java
ensureCapacityInternal(size + 1);
```
![ensureCapacityInternal方法](http://cdn.jayh.club/blog/20200828/103253283.png)
(2)計算陣列的當前容量calculateCapacity
```java
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
```
`minCapacity` : 值為1
`elementData`:代表當前陣列
我們先看ensureCapacityInternal呼叫的ensureCapacityInternal方法
```java
calculateCapacity(elementData, minCapacity)
```
calculateCapacity方法如下:
```java
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
```
`elementData`:代表當前陣列,新增第一個元素時,elementData等於DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空陣列)
`minCapacity`:等於1
`DEFAULT_CAPACITY`: 等於10
返回 Math.max(DEFAULT_CAPACITY, minCapacity) = 10
小結:所以第一次新增元素時,計算陣列的大小為10
(3)確定當前容量ensureExplicitCapacity
![ensureExplicitCapacity方法](http://cdn.jayh.club/blog/20200828/110617457.png)
minCapacity = 10
elementData.length=0
小結:因minCapacity > elementData.length,所以**進行第一次擴容,呼叫grow()方法從0擴大到10**。
(4)呼叫grow方法
![grow方法](http://cdn.jayh.club/blog/20200828/111339453.png)
oldCapacity=0,newCapacity=10。
然後執行 elementData = Arrays.copyOf(elementData, newCapacity);
將當前陣列和容量大小進行陣列拷貝操作,賦值給elementData。**陣列的容量設定為10**
elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值將會不一樣。
(5)然後將元素賦值給陣列第一個元素,且size自增1
``` java
elementData[size++] = e;
```
(6)新增第二個元素時,傳給ensureCapacityInternal的是2
```
ensureCapacityInternal(size + 1)
```
size=1,size+1=2
(7)第二次新增元素時,執行calculateCapacity
![mark](http://cdn.jayh.club/blog/20200828/112454671.png)
elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值不相等,所以直接返回2
(8)第二次新增元素時,執行ensureExplicitCapacity
因minCapacity等於2,小於當前陣列的長度10,所以不進行擴容,不執行grow方法。
![mark](http://cdn.jayh.club/blog/20200828/112704893.png)
(9)將第二個元素新增到陣列中,size自增1
```
elementData[size++] = e
```
(10)當新增第11個元素時呼叫grow方法進行擴容
![mark](http://cdn.jayh.club/blog/20200828/120912599.png)
minCapacity=11, elementData.length=10,呼叫grow方法。
`(11)擴容1.5倍`
```java
int newCapacity = oldCapacity + (oldCapacity >> 1);
```
oldCapacity=10,先換算成二級制1010,然後右移一位,變成0101,對應十進位制5,所以newCapacity=10+5=15,擴容1.5倍後是15。
![擴容1.5倍](http://cdn.jayh.club/blog/20200828/121108727.png)
(12)小結
- 1.ArrayList初始化為一個`空陣列`
- 2.ArrayList的Add操作不是執行緒安全的
- 3.ArrayList新增第一個元素時,陣列的容量設定為`10`
- 4.當ArrayList陣列超過當前容量時,擴容至`1.5倍`(遇到計算結果為小數的,向下取整),第一次擴容後,容量為15,第二次擴容至22...
- 5.ArrayList在第一次和擴容後都會對陣列進行拷貝,呼叫`Arrays.copyOf`方法。
![安全出行](http://cdn.jayh.club/blog/20200829/by1tKReqlQlP.png?imageslim)
### 1.3、ArrayList單執行緒環境是否安全?
**場景:**
我們通過一個`新增積木的例子`來說明單執行緒下ArrayList是執行緒安全的。
將 積木 `三角形A`、`四邊形B`、`五邊形C`、`六邊形D`、`五角星E`依次新增到一個盒子中,盒子中共有5個方格,每一個方格可以放一個積木。
![ArrayList單執行緒下新增元素](http://cdn.jayh.club/blog/20200827/150707103.png)
**程式碼實現:**
(1)這次我們用新的積木類`BuildingBlockWithName`
這個積木類可以傳形狀shape和名字name
```java
/**
* 積木類
* @author: 悟空聊架構
* @create: 2020-08-27
*/
class BuildingBlockWithName {
String shape;
String name;
public BuildingBlockWithName(String shape, String name) {
this.shape = shape;
this.name = name;
}
@Override
public String toString() {
return "BuildingBlockWithName{" + "shape='" + shape + ",name=" + name +'}';
}
}
```
(2)初始化一個ArrayList
```java
ArrayList arrayList = new ArrayList<>();
```
(3)依次新增三角形A、四邊形B、五邊形C、六邊形D、五角星E
```java
arrayList.add(new BuildingBlockWithName("三角形", "A"));
arrayList.add(new BuildingBlockWithName("四邊形", "B"));
arrayList.add(new BuildingBlockWithName("五邊形", "C"));
arrayList.add(new BuildingBlockWithName("六邊形", "D"));
arrayList.add(new BuildingBlockWithName("五角星", "E"));
```
(4)驗證`arrayList`中元素的內容和順序是否和新增的一致
``` java
BuildingBlockWithName{shape='三角形,name=A}
BuildingBlockWithName{shape='四邊形,name=B}
BuildingBlockWithName{shape='五邊形,name=C}
BuildingBlockWithName{shape='六邊形,name=D}
BuildingBlockWithName{shape='五角星,name=E}
```
我們看到結果確實是一致的。
**小結:** 單執行緒環境中,ArrayList是執行緒安全的。
### 1.4、多執行緒下ArrayList是不安全的
**場景如下:** 20個執行緒隨機往ArrayList新增一個任意形狀的積木。
![多執行緒場景往陣列存放元素](http://cdn.jayh.club/blog/20200828/084338941.png)
(1)程式碼實現:20個執行緒往陣列中隨機存放一個積木。
![多執行緒下ArrayList是不安全的](http://cdn.jayh.club/blog/20200827/154511673.png)
(2)列印結果:程式開始執行後,每個執行緒只存放一個隨機的積木。
![列印結果](http://cdn.jayh.club/blog/20200827/172244687.png)
陣列中會不斷存放積木,多個執行緒會爭搶陣列的存放資格,在存放過程中,會丟擲一個異常: `ConcurrentModificationException`(並行修改異常)
``` java
Exception in thread "10" Exception in thread "13" java.util.ConcurrentModificationException
```
![mark](http://cdn.jayh.club/blog/20200827/172451907.png)
這個就是常見的併發異常:java.util.ConcurrentModificationException
### 1.5 那如何解決ArrayList執行緒不安全問題呢?
有如下方案:
- 1.用Vector代替ArrayList
- 2.用Collections.synchronized(new ArrayList<>())
- 3.CopyOnWriteArrayList
### 1.6 Vector是保證執行緒安全的?
下面就來分析vector的原始碼。
#### 1.6.1 初始化Vector
初始化容量為10
```java
public Vector() {
this(10);
}
```
#### 1.6.2 Add操作是執行緒安全的
Add方法加了`synchronized`,來保證add操作是執行緒安全的(保證可見性、原子性、有序性),對這幾個概念有不懂的可以看下之前的寫的文章-》 [反制面試官 | 14張原理圖 | 再也不怕被問 volatile!](https://juejin.im/post/6861885337568804871)
![Add方法加了synchronized](http://cdn.jayh.club/blog/20200828/173019097.png)
#### 1.6.3 Vector擴容至2倍
```
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
```
![容量擴容至2倍](http://cdn.jayh.club/blog/20200828/171023634.png)
**注意:** capacityIncrement 在初始化的時候可以傳值,不傳則預設為0。如果傳了,則第一次擴容時為設定的oldCapacity+capacityIncrement,第二次擴容時擴大1倍。
**缺點:** 雖然保證了執行緒安全,但因為加了排斥鎖`synchronized`,會造成阻塞,所以**效能降低**。
![阻塞](http://cdn.jayh.club/blog/20200828/jUTSwB8cAuRs.png?imageslim)
#### 1.6.4 用積木模擬Vector的add操作
![vector的add操作](http://cdn.jayh.club/blog/20200828/175709345.png)
當往vector存放元素時,給盒子加了一個鎖,只有一個人可以存放積木,放完後,釋放鎖,放第二元素時,再進行加鎖,依次往復進行。
### 1.7 使用Collections.synchronizedList保證執行緒安全
我們可以使用Collections.synchronizedList方法來封裝一個ArrayList。
```
List