21张图带你领略集合的线程不安全
这是我的第 55 篇原创文章
作者 | 悟空聊架构
作者专注于用精美原理图解释底层原理
来源 | 悟空聊架构(ID:PassJava666)
本篇主要内容如下:

本篇所有示例代码
已更新到 我的Github
本篇文章已收纳到我的Java在线文档

一、线程不安全之ArrayList
集合框架有Map和Collection两大类,Collection下面有List、Set、Queue。List下面有ArrayList、Vector、LinkedList。如下图所示:

JUC并发包下的集合类Collections有Queue、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentMap

我们先来看看ArrayList。
1.1、ArrayList的底层初始化操作
首先我们来复习下ArrayList的使用,下面是初始化一个ArrayList,数组存放的是Integer类型的值。
new ArrayList();
那么底层做了什么操作呢?
1.2、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);

(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

minCapacity = 10
elementData.length=0
小结:因minCapacity > elementData.length,所以进行第一次扩容,调用grow()方法从0扩大到10。
(4)调用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

elementData的值和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的值不相等,所以直接返回2
(8)第二次添加元素时,执行ensureExplicitCapacity
因minCapacity等于2,小于当前数组的长度10,所以不进行扩容,不执行grow方法。

(9)将第二个元素添加到数组中,size自增1
elementData[size++] = e
(10)当添加第11个元素时调用grow方法进行扩容

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。

(12)小结
1.ArrayList初始化为一个 空数组
2.ArrayList的Add操作不是线程安全的 3.ArrayList添加第一个元素时,数组的容量设置为 10
4.当ArrayList数组超过当前容量时,扩容至 1.5倍
(遇到计算结果为小数的,向下取整),第一次扩容后,容量为15,第二次扩容至22...5.ArrayList在第一次和扩容后都会对数组进行拷贝,调用 Arrays.copyOf
方法。

1.3、ArrayList单线程环境是否安全?
场景:
我们通过一个添加积木的例子
来说明单线程下ArrayList是线程安全的。
将 积木 三角形A
、四边形B
、五边形C
、六边形D
、五角星E
依次添加到一个盒子中,盒子中共有5个方格,每一个方格可以放一个积木。

代码实现:
(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 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.4、多线程下ArrayList是不安全的
场景如下: 20个线程随机往ArrayList添加一个任意形状的积木。

(1)代码实现:20个线程往数组中随机存放一个积木。

(2)打印结果:程序开始运行后,每个线程只存放一个随机的积木。

数组中会不断存放积木,多个线程会争抢数组的存放资格,在存放过程中,会抛出一个异常: ConcurrentModificationException
(并行修改异常)
Exception in thread "10" Exception in thread "13" java.util.ConcurrentModificationException

这个就是常见的并发异常: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!

1.6.3 Vector扩容至2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);

注意: capacityIncrement 在初始化的时候可以传值,不传则默认为0。如果传了,则第一次扩容时为设置的oldCapacity+capacityIncrement,第二次扩容时扩大1倍。
缺点: 虽然保证了线程安全,但因为加了排斥锁synchronized
,会造成阻塞,所以性能降低。

1.6.4 用积木模拟Vector的add操作

当往vector存放元素时,给盒子加了一个锁,只有一个人可以存放积木,放完后,释放锁,放第二元素时,再进行加锁,依次往复进行。
1.7 使用Collections.synchronizedList保证线程安全
我们可以使用Collections.synchronizedList方法来封装一个ArrayList。
List