$1. 背景
在多线程高并发场景下,常用的集合类ArrayList、HashSet以及HashMap都经常被扣上一顶"不安全"的帽子,为什么呢?实际上,这三种常用集合类的读写方法中并没有加任何同步机制,从而会导致在同一时间有多个线程对集合进行写操作(如add,remove)和读操作(如get)。多个线程同时读还尚且能够接受,但同时写或者一边读一边写呢?上述集合都会基于add进行自动扩容,如果一条线程执行add操作触发容器扩容后还未进行实际写入,另一条线程就来读数据,可能读到的就是null值,在实际生产中将会导致严重的数据错误!
// 如下代码会发生数据同步错误
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Test {
public static void main(String[] args) throws InterruptedException {
List<Integer> list = new ArrayList<>();
for(int i=0; i<20; i++){
new Thread(()->{
list.add(new Random().nextInt()); // 写
System.out.println(list); // 读
},String.valueOf(i)).start();
}
Thread.sleep(1000);
System.out.println(list);
}
}
在Java中,对于高并发的场景,上面的这三种常用集合类肯定不能再使用了,因此需要选择线程更加安全的集合类来替代它们
$2. List
对于List而言,可用于替代ArrayList的线程安全集合类主要有如下三种:
- vector
- Collections.syncronizedList
- CopyOnWriteArrayList
2.1 Vector
古老的集合类,之所以线程安全是其中所有读写方法都加上了Syncronized同步锁机制,虽然能够保证数据的同步性,但实际上也变相地将并发变成了串行,即同一时间只能有一条线程进行集合的读或者写操作,性能因此非常的差
// Vector源码 -- 读写操作
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
2.2 Collections.syncronized()
借助Collections工具类的静态方法创建并返回一个阻塞的List对象,实质上是在Collection内部创建了一个静态内部类SyncronizedList,该静态内部类中所有读写操作使用了syncronized同步代码块,实质上和Vector类似,同一时间只支持单线程读 or 写,性能较低**
// Collections源码
public class Collections {
......
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
......
public void add(int index, E element) {
synchronized (mutex) {
list.add(index, element);
}
}
public E remove(int index) {
synchronized (mutex) {
return list.remove(index);
}
}
......
}
}
// 获得Collections.syncronizedList对象
List<Integer> list3 = Collections.synchronizedList(new ArrayList<>());
2.3 CopyOnWriteArrayList
CopyOnWriteArrayList实现的是写时复制的思想:往容器中添加元素时,并不是直接往当前容器数组中添加,而是先将当前容器数据在底层复制一份,然后进行扩容+1并将元素添加到新的容器中,添加完元素后,将原容器的引用指向新的容器,原容器对象会被GC。这样做的好处是可以对CopyOnWrite容器进行并发的读操作而不需要加锁,因为当前容器不会添加任何元素。CopyOnWrite容器是一种读写分离的思想。
注意:CopyOnWriteArrayList允许多线程并发读,但同一时间只允许被一个线程修改,原因是每个修改方法都加了lock锁
// CopyOnWriteArrayList源码 - 读写操作
public boolean add(E e){
final ReentrantLock lock = this.lock;
lock.lock(); // 加同步锁
try{
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len+1);
newElements[len] = e; // 写入当前元素 并每次扩容一个单位
setArray(newElements); // 将加上新元素的数组设置为新的引用对象,取代旧版本
return true;
}finally{
lock.unlock();
}
}
public E get(int index) { // 无锁
return get(getArray(), index);
}
CopyOnWriteArrayList在高并发场景下性能一般最优,因为其支持并发的读操作,不会产生大量线程阻塞的情况
$3. Set
对于Set而言,可用于替代HashSet的线程安全集合类主要有如下三种:
- Collections.syncronizedSet
- CopyOnWriteArraySet
3.1 Collections.syncronizedSet()
原理和实现机制与Collections.syncronizedSet一致
3.1 CopyOnWriteArraySet
这里很容易因为先入为主而产生一个误区:HashSet的底层是实现的HashMap,但是 CopyOnWriteArraySet底层实现的是CopyOnWriteArrayList!!!和Map毫无关系!!!
看代码 --->
// CopyOnWriteArraySet 源码
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() { // 构造方法1
al = new CopyOnWriteArrayList<E>();
}
public CopyOnWriteArraySet(Collection<? extends E> c) {
if (c.getClass() == CopyOnWriteArraySet.class) {
@SuppressWarnings("unchecked") CopyOnWriteArraySet<E> cc =
(CopyOnWriteArraySet<E>)c;
al = new CopyOnWriteArrayList<E>(cc.al);
}
else {
al = new CopyOnWriteArrayList<E>();
al.addAllAbsent(c);
// 调用CopyOnWriteArrayList的addAllAbsent方法,将c的非重复元素加入al
}
}
public boolean add(E e) {
return al.addIfAbsent(e); // CopyOnWriteArrayList的 add + 去重功能
}
}
// CopyOnWriteArrayList源码之实现去重插入 ---> 为CopyOnWriteArraySet准备的
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray(); // 备份add前的数组
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0) // 即元素已存在,放弃
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
$4. Map
对于Map而言,可用于替代HashMap的线程安全集合类主要有如下三种:
- HashTable
- Collections.syncronizedMap
- ConcurrentHashMap
4.1 HashTable
古老的Map集合,和Vector类似,所有的读写方法都加入了Syncronized同步锁,多线程下性能很低
4.2 Collections.syncronizedMap
同Collections.syncronizedList、Collections.syncronizedSet
4.3 ConcurrentHashMap
思想:锁机制更加颗粒化 -- 实质上这是高并发场景下保持数据同步性和并发性能平衡的核心诉求
- JDK1.7
ConcurrentHashMap的底层数据结构如下图:
该版本的ConcurrentHashMap使用的是ReentrantLock锁,但锁的对象并不是整个Map,而是底层数组的每一个segment,其中每一个segment实质上就相当于一个HashTable,以相对臃肿的结构变相地实现了锁的细化
- JDK1.8
底层数据结构发生了改变:
在jdk1.8中,ConcurrentHashMap放弃了segment数组的臃肿设计,采用Node数组 + CAS + syncronized锁的机制实现并发同步,并且也引进了红黑树结构,当一个数组索引位置的链表长度达到8后就会转化为红黑树结构,优化了后续查询速度
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。