超详细 | 21张图带你领略集合的线程不安全
The following article is from 悟空聊架构 Author 悟空聊架构
线程不安全之ArrayList
1.1、ArrayList 的底层初始化操作
new ArrayList<Integer>();
1.2、ArrayList 的底层原理
1.2.1 初始化数组
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
1.2.2 ArrayList 的 add 操作
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
1.2.3 ArrayList 扩容源码解析
ensureCapacityInternal(size + 1);
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
calculateCapacity(elementData, minCapacity)
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
elementData[size++] = e;
ensureCapacityInternal(size + 1)
elementData[size++] = e
1.ArrayList初始化为一个空数组。 2.ArrayList的Add操作不是线程安全的。 3.ArrayList添加第一个元素时,数组的容量设置为10。 4.当ArrayList数组超过当前容量时,扩容至1.5倍(遇到计算结果为小数的,向下取整),第一次扩容后,容量为15,第二次扩容至22... 5.ArrayList在第一次和扩容后都会对数组进行拷贝,调用Arrays.copyOf方法。
1.3、ArrayList单线程环境是否安全?
/**
* 积木类
* @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 +'}';
}
}
ArrayList<BuildingBlock> arrayList = new ArrayList<>();
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"));
BuildingBlockWithName{shape='三角形,name=A}
BuildingBlockWithName{shape='四边形,name=B}
BuildingBlockWithName{shape='五边形,name=C}
BuildingBlockWithName{shape='六边形,name=D}
BuildingBlockWithName{shape='五角星,name=E}
1.4、多线程下ArrayList是不安全的
Exception in thread "10" Exception in thread "13" java.util.ConcurrentModificationException
1.5 那如何解决 ArrayList 线程不安全问题呢?
用Vector代替ArrayList 用Collections.synchronized(new ArrayList<>()) CopyOnWriteArrayList
1.6 Vector 是保证线程安全的?
1.6.1 初始化 Vector
public Vector() {
this(10);
}
1.6.2 Add 操作是线程安全的
1.6.3 Vector 扩容至2倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
1.6.4 用积木模拟Vector的add操作
1.7 使用 Collections.synchronizedList 保证线程安全
List<Object> arrayList = Collections.synchronizedList(new ArrayList<>());
synchronized (list) {
Iterator i = list.iterator(); // Must be in synchronized block
while (i.hasNext())
foo(i.next());
}
1.8 使用 CopyOnWriteArrayList 保证线程安全
1.8.1 CopyOnWriteArrayList思想
Copy on write:写时复制,一种读写分离的思想。 写操作:添加元素时,不直接往当前容器添加,而是先拷贝一份数组,在新的数组中添加元素后,在将原容器的引用指向新的容器。因为数组时用volatile关键字修饰的,所以当array重新赋值后,其他线程可以立即知道(volatile的可见性)。 读操作:读取数组时,读老的数组,不需要加锁。 读写分离:写操作是copy了一份新的数组进行写,读操作是读老的数组,所以是读写分离。
1.8.2 使用方式
CopyOnWriteArrayList<BuildingBlockWithName> arrayList = new CopyOnWriteArrayList<>();
1.8.3 底层源码分析
先定义了一个可重入锁 ReentrantLock。 添加元素前,先获取锁lock.lock()。 添加元素时,先拷贝当前数组 Arrays.copyOf。 添加元素时,扩容+1(len + 1)。 添加元素后,将数组引用指向新加了元素后的数组setArray(newElements)。
private transient volatile Object[] array;
1.8.4 ReentrantLock 和synchronized的区别
1.都是用来协调多线程对共享对象、变量的访问。 2.都是可重入锁,同一线程可以多次获得同一个锁。 3.都保证了可见性和互斥性。
1.ReentrantLock 显示的获得、释放锁, synchronized 隐式获得释放锁。 2.ReentrantLock 可响应中断, synchronized 是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性。 3.ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的。 4.ReentrantLock 可以实现公平锁、非公平锁。 5.ReentrantLock 通过 Condition 可以绑定多个条件。 6.底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略, lock 是同步非阻塞,采用的是乐观并发策略。
1.8.5 Lock和synchronized的区别
1.Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别。 2.Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。 3.synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。 4.Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。 5.通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。 6.Lock 可以通过实现读写锁提高多个线程进行读操作的效率。
线程不安全之 HashSet
2.1 HashSet的用法
Set<BuildingBlockWithName> Set = new HashSet<>();
set.add("a");
2.2 HashSet的底层原理
public HashSet() {
map = new HashMap<>();
}
2.3 HashSet的add操作
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。
2.4 如何保证线程安全
1.使用 Collections.synchronizedSet
Set<BuildingBlockWithName> set = Collections.synchronizedSet(new HashSet<>());
CopyOnWriteArraySet<BuildingBlockWithName> set = new CopyOnWriteArraySet<>();
2.5 CopyOnWriteArraySet 的底层还是使用的是 CopyOnWriteArrayList
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
线程不安全之HashMap
3.1 HashMap 的使用
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原理
其他的集合类
总结
Vector是通过在add等方法前加synchronized来保证线程安全。 Collections.synchronized()是通过包装数组,在数组的操作方法前加synchronized来保证线程安全。 CopyOnWriteArrayList通过写时复制来保证线程安全的。
Collections.synchronizedSet CopyOnWriteArraySet
Collections.synchronizedMap ConcurrentHashMap
更多阅读推荐