本篇主要內容以下:java
![](http://static.javashuo.com/static/loading.gif)
本篇主要內容面試
本篇全部示例代碼
已更新到 個人Github數組
本篇文章已收納到個人Java在線文檔安全
![](http://static.javashuo.com/static/loading.gif)
集合,準備團戰微信
1、線程不安全之ArrayList
集合框架有Map和Collection兩大類,Collection下面有List、Set、Queue。List下面有ArrayList、Vector、LinkedList。以下圖所示:多線程
![](http://static.javashuo.com/static/loading.gif)
集合框架思惟導圖架構
JUC併發包下的集合類Collections有Queue、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentMap併發
![](http://static.javashuo.com/static/loading.gif)
JUC包下的Collections框架
咱們先來看看ArrayList。ide
1.一、ArrayList的底層初始化操做
首先咱們來複習下ArrayList的使用,下面是初始化一個ArrayList,數組存放的是Integer類型的值。
new ArrayList<Integer>();
那麼底層作了什麼操做呢?
1.二、ArrayList的底層原理
1.2.1 初始化數組
/** * 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操做
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操做時,會先確認是否超過數組大小
ensureCapacityInternal(size + 1);
![](http://static.javashuo.com/static/loading.gif)
ensureCapacityInternal方法
(2)計算數組的當前容量calculateCapacity
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
minCapacity
: 值爲1
elementData
:表明當前數組
咱們先看ensureCapacityInternal調用的ensureCapacityInternal方法
calculateCapacity(elementData, minCapacity)
calculateCapacity方法以下:
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
![](http://static.javashuo.com/static/loading.gif)
ensureExplicitCapacity方法
minCapacity = 10
elementData.length=0
小結:因minCapacity > elementData.length,因此進行第一次擴容,調用grow()方法從0擴大到10。
(4)調用grow方法
![](http://static.javashuo.com/static/loading.gif)
grow方法
oldCapacity=0,newCapacity=10。
而後執行 elementData = Arrays.copyOf(elementData, newCapacity);
將當前數組和容量大小進行數組拷貝操做,賦值給elementData。數組的容量設置爲10
elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值將會不同。
(5)而後將元素賦值給數組第一個元素,且size自增1
elementData[size++] = e;
(6)添加第二個元素時,傳給ensureCapacityInternal的是2
ensureCapacityInternal(size + 1)
size=1,size+1=2
(7)第二次添加元素時,執行calculateCapacity
![](http://static.javashuo.com/static/loading.gif)
mark
elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值不相等,因此直接返回2
(8)第二次添加元素時,執行ensureExplicitCapacity
因minCapacity等於2,小於當前數組的長度10,因此不進行擴容,不執行grow方法。
![](http://static.javashuo.com/static/loading.gif)
mark
(9)將第二個元素添加到數組中,size自增1
elementData[size++] = e
(10)當添加第11個元素時調用grow方法進行擴容
![](http://static.javashuo.com/static/loading.gif)
mark
minCapacity=11, elementData.length=10,調用grow方法。
(11)擴容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
oldCapacity=10,先換算成二級制1010,而後右移一位,變成0101,對應十進制5,因此newCapacity=10+5=15,擴容1.5倍後是15。
![](http://static.javashuo.com/static/loading.gif)
擴容1.5倍
(12)小結
-
1.ArrayList初始化爲一個
空數組
-
2.ArrayList的Add操做不是線程安全的
-
3.ArrayList添加第一個元素時,數組的容量設置爲
10
-
4.當ArrayList數組超過當前容量時,擴容至
1.5倍
(遇到計算結果爲小數的,向下取整),第一次擴容後,容量爲15,第二次擴容至22... -
5.ArrayList在第一次和擴容後都會對數組進行拷貝,調用
Arrays.copyOf
方法。
![](http://static.javashuo.com/static/loading.gif)
安全出行
1.三、ArrayList單線程環境是否安全?
場景:
咱們經過一個添加積木的例子
來講明單線程下ArrayList是線程安全的。
將 積木 三角形A
、四邊形B
、五邊形C
、六邊形D
、五角星E
依次添加到一個盒子中,盒子中共有5個方格,每個方格能夠放一個積木。
![](http://static.javashuo.com/static/loading.gif)
ArrayList單線程下添加元素
代碼實現:
(1)此次咱們用新的積木類BuildingBlockWithName
這個積木類能夠傳形狀shape和名字name
/** * 積木類 * @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
ArrayList<BuildingBlock> arrayList = new ArrayList<>();
(3)依次添加三角形A、四邊形B、五邊形C、六邊形D、五角星E
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
中元素的內容和順序是否和添加的一致
BuildingBlockWithName{shape='三角形,name=A} BuildingBlockWithName{shape='四邊形,name=B} BuildingBlockWithName{shape='五邊形,name=C} BuildingBlockWithName{shape='六邊形,name=D} BuildingBlockWithName{shape='五角星,name=E}
咱們看到結果確實是一致的。
小結: 單線程環境中,ArrayList是線程安全的。
1.四、多線程下ArrayList是不安全的
場景以下: 20個線程隨機往ArrayList添加一個任意形狀的積木。
![](http://static.javashuo.com/static/loading.gif)
多線程場景往數組存放元素
(1)代碼實現:20個線程往數組中隨機存放一個積木。
![](http://static.javashuo.com/static/loading.gif)
多線程下ArrayList是不安全的
(2)打印結果:程序開始運行後,每一個線程只存放一個隨機的積木。
![](http://static.javashuo.com/static/loading.gif)
打印結果
數組中會不斷存放積木,多個線程會爭搶數組的存放資格,在存放過程當中,會拋出一個異常: ConcurrentModificationException
(並行修改異常)
Exception in thread "10" Exception in thread "13" java.util.ConcurrentModificationException
![](http://static.javashuo.com/static/loading.gif)
mark
這個就是常見的併發異常: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
public Vector() { this(10); }
1.6.2 Add操做是線程安全的
Add方法加了synchronized
,來保證add操做是線程安全的(保證可見性、原子性、有序性),對這幾個概念有不懂的能夠看下以前的寫的文章-》 反制面試官 | 14張原理圖 | 不再怕被問 volatile!
![](http://static.javashuo.com/static/loading.gif)
Add方法加了synchronized
1.6.3 Vector擴容至2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
![](http://static.javashuo.com/static/loading.gif)
容量擴容至2倍
注意: capacityIncrement 在初始化的時候能夠傳值,不傳則默認爲0。若是傳了,則第一次擴容時爲設置的oldCapacity+capacityIncrement,第二次擴容時擴大1倍。
缺點: 雖然保證了線程安全,但由於加了排斥鎖synchronized
,會形成阻塞,因此性能下降。
![](http://static.javashuo.com/static/loading.gif)
阻塞
1.6.4 用積木模擬Vector的add操做
![](http://static.javashuo.com/static/loading.gif)
vector的add操做
當往vector存放元素時,給盒子加了一個鎖,只有一我的能夠存放積木,放完後,釋放鎖,放第二元素時,再進行加鎖,依次往復進行。
1.7 使用Collections.synchronizedList保證線程安全
咱們可使用Collections.synchronizedList方法來封裝一個ArrayList。
List<Object> arrayList = Collections.synchronizedList(new ArrayList<>());
爲何這樣封裝後,就是線程安全的?
源碼解析: 由於Collections.synchronizedList封裝後的list,list的全部操做方法都是帶synchronized
關鍵字的(除iterator()以外),至關於全部操做都會進行加鎖,因此使用它是線程安全的(除迭代數組以外)。
![](http://static.javashuo.com/static/loading.gif)
加鎖
![](http://static.javashuo.com/static/loading.gif)
mark
注意: 當迭代數組時,須要手動作同步。官方示例以下:
synchronized (list) { Iterator i = list.iterator(); // Must be in synchronized block while (i.hasNext()) foo(i.next()); }
1.8 使用CopyOnWriteArrayList保證線程安全
![](http://static.javashuo.com/static/loading.gif)
複製
1.8.1 CopyOnWriteArrayList思想
-
Copy on write:寫時複製,一種讀寫分離的思想。
-
寫操做:添加元素時,不直接往當前容器添加,而是先拷貝一份數組,在新的數組中添加元素後,在將原容器的引用指向新的容器。由於數組時用volatile關鍵字修飾的,因此當array從新賦值後,其餘線程能夠當即知道(volatile的可見性)
-
讀操做:讀取數組時,讀老的數組,不須要加鎖。
-
讀寫分離:寫操做是copy了一份新的數組進行寫,讀操做是讀老的數組,因此是讀寫分離。
1.8.2 使用方式
CopyOnWriteArrayList<BuildingBlockWithName> arrayList = new CopyOnWriteArrayList<>();
1.8.3 底層源碼分析
![](http://static.javashuo.com/static/loading.gif)
CopyOnWriteArrayList的add方法分析
add的流程:
-
先定義了一個可重入鎖
ReentrantLock
-
添加元素前,先獲取鎖
lock.lock()
-
添加元素時,先拷貝當前數組
Arrays.copyOf
-
添加元素時,擴容+1(
len + 1
) -
添加元素後,將數組引用指向新加了元素後的數組
setArray(newElements)
爲何數組從新賦值後,其餘線程能夠當即知道?
由於這裏的數組是用volatile修飾的,哇,又是volatile
,這個關鍵字真妙^_^
private transient volatile Object[] array;
![](http://static.javashuo.com/static/loading.gif)
妙啊
1.8.4 ReentrantLock 和synchronized的區別
劃重點
相同點:
-
1.都是用來協調多線程對共享對象、變量的訪問
-
2.都是可重入鎖,同一線程能夠屢次得到同一個鎖
-
3.都保證了可見性和互斥性
不一樣點:
![](http://static.javashuo.com/static/loading.gif)
樂觀
-
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://static.javashuo.com/static/loading.gif)
自動擋和手動擋的區別
-
1.Lock須要手動獲取鎖和釋放鎖。就比如自動擋和手動擋的區別
-
1.Lock 是一個接口,而 synchronized 是 Java 中的關鍵字, synchronized 是內置的語言實現。
-
2.synchronized 在發生異常時,會自動釋放線程佔有的鎖,所以不會致使死鎖現象發生;而 Lock 在發生異常時,若是沒有主動經過 unLock()去釋放鎖,則極可能形成死鎖現象,所以使用 Lock 時須要在 finally 塊中釋放鎖。
-
3.Lock 可讓等待鎖的線程響應中斷,而 synchronized 卻不行,使用 synchronized 時,等待的線程會一直等待下去,不可以響應中斷。
-
4.經過 Lock 能夠知道有沒有成功獲取鎖,而 synchronized 卻沒法辦到。
-
5.Lock 能夠經過實現讀寫鎖提升多個線程進行讀操做的效率。
2、線程不安全之HashSet
有了前面大篇幅的講解ArrayList的線程不安全,以及如何使用其餘方式來保證線程安全,如今講HashSet應該更容易理解一些。
2.1 HashSet的用法
用法以下:
Set<BuildingBlockWithName> 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操做
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
Set<BuildingBlockWithName> set = Collections.synchronizedSet(new HashSet<>());
-
2.使用CopyOnWriteArraySet
CopyOnWriteArraySet<BuildingBlockWithName> set = new CopyOnWriteArraySet<>();
2.5 CopyOnWriteArraySet的底層仍是使用的是CopyOnWriteArrayList
public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList<E>(); }
3、線程不安全之HashMap
3.1 HashMap的使用
同理,HashMap和HashSet同樣,在多線程環境下也是線程不安全的。
Map<String, BuildingBlockWithName> map = new HashMap<>(); map.put("A", new BuildingBlockWithName("三角形", "A"));
3.2 HashMap線程不安全解決方案:
-
1.Collections.synchronizedMap
Map<String, BuildingBlockWithName> map2 = Collections.synchronizedMap(new HashMap<>());
-
2.ConcurrentHashMap
ConcurrentHashMap<String, BuildingBlockWithName> set3 = new ConcurrentHashMap<>();
3.3 ConcurrentHashMap原理
ConcurrentHashMap,它內部細分了若干個小的 HashMap,稱之爲段(Segment)。默認狀況下一個 ConcurrentHashMap 被進一步細分爲 16 個段,既就是鎖的併發度。若是須要在 ConcurrentHashMap 中添加一個新的表項,並非將整個 HashMap 加鎖,而是首先根據 hashcode 獲得該表項應該存放在哪一個段中,而後對該段加鎖,並完成 put 操做。在多線程環境中,若是多個線程同時進行put操做,只要被加入的表項不存放在同一個段中,則線程間能夠作到真正的並行。
4、其餘的集合類
LinkedList: 線程不安全,同ArrayListTreeSet: 線程不安全,同HashSetLinkedHashSet: 線程不安全,同HashSetTreeMap: 同HashMap,線程不安全HashTable: 線程安全
總結
本篇第一個部分詳細講述了ArrayList集合的底層擴容原理,演示了ArrayList的線程不安全會致使拋出併發修改異常
。而後經過源碼解析的方式講解了三種方式來保證線程安全:
-
Vector
是經過在add
等方法前加synchronized
來保證線程安全 -
Collections.synchronized()
是經過包裝數組,在數組的操做方法前加synchronized
來保證線程安全 -
CopyOnWriteArrayList
經過寫時複製
來保證線程安全的。
第二部分講解了HashSet的線程不安全性,經過兩種方式保證線程安全:
-
Collections.synchronizedSet
-
CopyOnWriteArraySet
第三部分講解了HashMap的線程不安全性,經過兩種方式保證線程安全:
-
Collections.synchronizedMap
-
ConcurrentHashMap
另外在講解的過程當中,也詳細對比了ReentrantLock和synchronized及Lock和synchronized的區別。
彩蛋: 聰明的你,必定發現集合裏面還漏掉了一個重要的東西:那就是Queue
。期待後續麼?
白嫖麼?轉發->在看->點贊-收藏!!!
我是悟空,一隻努力變強的碼農!我要變身超級賽亞人啦!
- END -
本文分享自微信公衆號 - 悟空聊架構(PassJava666)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。