1. 程式人生 > ># 全網最細 | 21張圖帶你領略集合的執行緒不安全

# 全網最細 | 21張圖帶你領略集合的執行緒不安全

# 全網最細 | 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 arrayList = Collections.synchronizedList(new ArrayList<>()); ``` 為什麼這樣封裝後,就是執行緒安全的? **原始碼解析:** 因為Collections.synchronizedList封裝後的list,list的所有操作方法都是帶`synchronized`關鍵字的(除iterator()之外),相當於所有操作都會進行加鎖,所以使用它是執行緒安全的(除迭代陣列之外)。 ![加鎖](http://cdn.jayh.club/blog/20200828/Nm0SXSoQvCfC.png?imageslim) ![mark](http://cdn.jayh.club/blog/20200828/QO9wUIByis9W.png?imageslim) **注意:** 當迭代陣列時,需要手動做同步。官方示例如下: ```java synchronized (list) { Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); } ``` ### 1.8 使用CopyOnWriteArrayList保證執行緒安全 ![複製](http://cdn.jayh.club/blog/20200828/1YkJT4tlYzow.png?imageslim) #### 1.8.1 CopyOnWriteArrayList思想 - Copy on write:寫時複製,一種讀寫分離的思想。 - 寫操作:新增元素時,不直接往當前容器新增,而是先拷貝一份陣列,在新的陣列中新增元素後,在將原容器的引用指向新的容器。因為陣列時用volatile關鍵字修飾的,所以當array重新賦值後,其他執行緒可以立即知道(volatile的可見性) - 讀操作:讀取陣列時,讀老的陣列,不需要加鎖。 - 讀寫分離:寫操作是copy了一份新的陣列進行寫,讀操作是讀老的陣列,所以是讀寫分離。 #### 1.8.2 使用方式 ```java CopyOnWriteArrayList arrayList = new CopyOnWriteArrayList<>(); ``` #### 1.8.3 底層原始碼分析 ![CopyOnWriteArrayList的add方法分析](http://cdn.jayh.club/blog/20200828/axPEk62UvBTN.png?imageslim) **add的流程:** - 先定義了一個可重入鎖 `ReentrantLock` - 新增元素前,先獲取鎖`lock.lock()` - 新增元素時,先拷貝當前陣列 `Arrays.copyOf` - 新增元素時,擴容+1(`len + 1`) - 新增元素後,將陣列引用指向新加了元素後的陣列`setArray(newElements)` 為什麼陣列重新賦值後,其他執行緒可以立即知道? 因為這裡的陣列是用volatile修飾的,`哇,又是volatile`,這個關鍵字真妙^_^ ``` java private transient volatile Object[] array; ``` ![妙啊](http://cdn.jayh.club/blog/20200828/9GzmvnjuBe5V.gif) #### 1.8.4 ReentrantLock 和synchronized的區別 `劃重點` **相同點:** - 1.都是用來協調多執行緒對共享物件、變數的訪問 - 2.都是可重入鎖,同一執行緒可以多次獲得同一個鎖 - 3.都保證了可見性和互斥性 **不同點:** ![樂觀](http://cdn.jayh.club/blog/20200829/tY9VNrsUYlDv.png?imageslim) - 1.ReentrantLock 顯示的獲得、釋放鎖, synchronized 隱式獲得釋放鎖 - 2.ReentrantLock 可響應中斷, synchronized 是不可以響應中斷的,為處理鎖的不可用性提供了更高的靈活性 - 3.ReentrantLock 是 API 級別的, synchronized 是 JVM 級別的 - 4.ReentrantLock 可以實現公平鎖、非公平鎖 - 5.ReentrantLock 通過 Condition 可以繫結多個條件 - 6.底層實現不一樣, synchronized 是同步阻塞,使用的是悲觀併發策略, lock 是同步非阻塞,採用的是樂觀併發策略 #### 1.8.5 Lock和synchronized的區別 ![自動擋和手動擋的區別](http://cdn.jayh.club/blog/20200829/qSlM4sQyhCTs.png?imageslim) - 1.Lock需要手動獲取鎖和釋放鎖。就好比自動擋和手動擋的區別 - 1.Lock 是一個介面,而 synchronized 是 Java 中的關鍵字, synchronized 是內建的語言實現。 - 2.synchronized 在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;而 Lock 在發生異常時,如果沒有主動通過 unLock()去釋放鎖,則很可能造成死鎖現象,因此使用 Lock 時需要在 finally 塊中釋放鎖。 - 3.Lock 可以讓等待鎖的執行緒響應中斷,而 synchronized 卻不行,使用 synchronized 時,等待的執行緒會一直等待下去,不能夠響應中斷。 - 4.通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到。 - 5.Lock 可以通過實現讀寫鎖提高多個執行緒進行讀操作的效率。 ## 二、執行緒不安全之HashSet 有了前面大篇幅的講解ArrayList的執行緒不安全,以及如何使用其他方式來保證執行緒安全,現在講HashSet應該更容易理解一些。 ### 2.1 HashSet的用法 用法如下: ```java Set Set = new HashSet<>(); set.add("a"); ``` 初始容量=10,負載因子=0.75(當元素個數達到容量的75%,啟動擴容) #### 2.2 HashSet的底層原理 ``` public HashSet() { map = new HashMap<>(); } ``` 底層用的還是HashMap()。 **考點:** 為什麼HashSet的add操作只用傳一個引數(value),而HashMap需要傳兩個引數(key和value) ### 2.3 HashSet的add操作 ```java private static final Object PRESENT = new Object(); public boolean add(E e) { return map.put(e, PRESENT)==null; } ``` **考點回答:** 因為HashSet的add操作中,key等於傳的value值,而value是PRESENT,PRESENT是new Object();,所以傳給map的是 key=e, value=new Object。Hash只關心key,不考慮value。 **為什麼HashSet不安全:** 底層add操作不保證可見性、原子性。所以不是執行緒安全的。 ### 2.4 如何保證執行緒安全 - 1.使用Collections.synchronizedSet ``` java Set set = Collections.synchronizedSet(new HashSet<>()); ``` - 2.使用CopyOnWriteArraySet ``` CopyOnWriteArraySet set = new CopyOnWriteArraySet<>(); ``` ### 2.5 CopyOnWriteArraySet的底層還是使用的是CopyOnWriteArrayList ```java public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList(); } ``` ## 三、執行緒不安全之HashMap ### 3.1 HashMap的使用 同理,HashMap和HashSet一樣,在多執行緒環境下也是執行緒不安全的。 ```java Map map = new HashMap<>(); map.put("A", new BuildingBlockWithName("三角形", "A")); ``` ### 3.2 HashMap執行緒不安全解決方案: - 1.Collections.synchronizedMap ``` Map map2 = Collections.synchronizedMap(new HashMap<>()); ``` - 2.ConcurrentHashMap ``` ConcurrentHashMap set3 = new ConcurrentHashMap<>(); ``` ### 3.3 ConcurrentHashMap原理 ConcurrentHashMap,它內部細分了若干個小的 HashMap,稱之為段(Segment)。 預設情況下一個 ConcurrentHashMap 被進一步細分為 16 個段,既就是鎖的併發度。如果需要在 ConcurrentHashMap 中新增一個新的表項,並不是將整個 HashMap 加鎖,而是首先根據 hashcode 得到該表項應該存放在哪個段中,然後對該段加鎖,並完成 put 操作。在多執行緒環境中,如果多個執行緒同時進行put操作,只要被加入的表項不存放在同一個段中,則執行緒間可以做到真正的並行。 ## 四、其他的集合類 **LinkedList:** 執行緒不安全,同ArrayList **TreeSet:** 執行緒不安全,同HashSet **LinkedHashSet:** 執行緒不安全,同HashSet **TreeMap:** 同HashMap,執行緒不安全 **HashTable:** 執行緒安全 ## 總結 本篇第一個部分詳細講述了ArrayList集合的底層擴容原理,演示了ArrayList的執行緒不安全會導致丟擲`併發修改異常`。然後通過原始碼解析的方式講解了三種方式來保證執行緒安全: - `Vector`是通過在`add`等方法前加`synchronized`來保證執行緒安全 - `Collections.synchronized()`是通過包裝陣列,在陣列的操作方法前加`synchronized`來保證執行緒安全 - `CopyOnWriteArrayList`通過`寫時複製`來保證執行緒安全的。 第二部分講解了HashSet的執行緒不安全性,通過兩種方式保證執行緒安全: - Collections.synchronizedSet - CopyOnWriteArraySet 第三部分講解了HashMap的執行緒不安全性,通過兩種方式保證執行緒安全: - Collections.synchronizedMap - ConcurrentHashMap 另外在講解的過程中,也詳細對比了ReentrantLock和synchronized及Lock和synchronized的區別。 **彩蛋:** 聰明的你,一定發現集合裡面還漏掉了一個**重要的東西**:那就是`Queue`。期待後續麼? **白嫖麼?轉發->在看->點贊-收藏!!!** 我是悟空,一隻努力變強的碼農!我要變身超級賽亞人啦!
![悟空](http://cdn.jayh.club/blog/20200829/xwhSItFPN0jI.png?imageslim) > 另外可以搜尋「悟空聊架構」或者PassJava666,一起進步! > 我的[GitHub主頁](https://github.com/Jackson0714),關注我的`Spring Cloud` 實戰專案[《佳必過》](https://github.com/Jackson0714/PassJava-Platform)