ArrayList、Set线程不安全解决方案
1.概述在日常开发中,ArrayList、Set在大量的场景下使用,然而我们都知道ArrayList和Set是线程不安全的,那么为什么ArrayList和Set是线程不安全的?如何保证在高并发场景下其线程安全性呢?本文将通过以下案例来剖析ArrayList、Set线程不安全的原因及现象,以及避免线程不安全的几种方案,并通过分析源码来阐述为什么这些方案能保证线程安全。2.测试及解决方案2.1 关于A
1.概述
在日常开发中,ArrayList、Set在大量的场景下使用,然而我们都知道ArrayList和Set是线程不安全的,那么为什么ArrayList和Set是线程不安全的?如何保证在高并发场景下其线程安全性呢?本文将通过以下案例来剖析ArrayList、Set线程不安全的原因及现象,以及避免线程不安全的几种方案,并通过分析源码来阐述为什么这些方案能保证线程安全。
2.测试及解决方案
2.1 关于ArrayList的测试代码
public class ArrayListTest {
public static void main(String[] args) {
List<String> strings = Arrays.asList("1", "2", "3");
List<String> lists = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(() -> {
String value = UUID.randomUUID().toString().substring(0,5);
lists.add(value);
System.out.println(lists);
},String.valueOf(i)).start();
}
}
}
2.2 测试结果
上述代码可能会出现以下并发修改异常,具体如下图所示:
2.3 分析原因
首先来看一下ArrayList的add方法底层源码(基于jdk1.8):
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
这段代码在高并发下大概率会出现问题的,同一时刻多个线程同时进入执行,其中一个线程add正常,其它线程必然无法正常add,因此就会异常。
2.4 解决方案
2.4.1 解决方案一
将上述代码中: List<String> lists = new ArrayList<>();
改为: List<String> lists = new Vector<>();
为什么利用Vector代替ArrayList生成的对象就能避免上述问题呢?首先来看下Vector和ArrayList的关系图,具体如下所示:
由上述类图可知,Vector和ArrayList均继承于AbstractList,且底层都是通过数组去实现对元素的快速存取。数组的缺点显而易见,可以快速获取指定位置的数据,适合遍历,但在数据存储时,效率较低。再来看一下Vector的add方法,具体如下:
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
这两个add方法的实现大同小异,区别在于synchronized 关键字的使用,synchronized 的作用毋庸置疑,保证某一时刻只能有一个线程执行该代码块,因此Vector是线程安全的。其实通过查看Vector与ArrayList的源码可以发现,两个类大部分关键方法都是类似的,区别在于Vector在类似的方法上都加了关键字synchronized来保证线程安全。
这种解决方案的缺陷是:
会降低线程执行效率(肯定的麦,小猪都知道,本来是一下就能执行,这要排队执行,那效率能不低),还有就是加锁和释放锁的这个过程,在系统中是有开销的,因此Vector效率相对较低。
2.4.2 解决方案二
将上述代码中:List<String> lists = new ArrayList<>();
替换为:List<String> strings = Collections.synchronizedList(new ArrayList<>());
接下来分析一下为什么通过这中种方案就可以变得线程安全呢?首先我们来看下synchronizedList这个部分的源码:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
由上述源码可以看出这是一个静态方法,通过传入的list类型来生成SynchronizedRandomAccessList还是SynchronizedList,接下来再看一下SynchronizedList的源码:
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedList<>(list.subList(fromIndex, toIndex),
mutex);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
synchronized (mutex) {list.replaceAll(operator);}
}
@Override
public void sort(Comparator<? super E> c) {
synchronized (mutex) {list.sort(c);}
}
/**
* SynchronizedRandomAccessList instances are serialized as
* SynchronizedList instances to allow them to be deserialized
* in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
* This method inverts the transformation. As a beneficial
* side-effect, it also grafts the RandomAccess marker onto
* SynchronizedList instances that were serialized in pre-1.4 JREs.
*
* Note: Unfortunately, SynchronizedRandomAccessList instances
* serialized in 1.4.1 and deserialized in 1.4 will become
* SynchronizedList instances, as this method was missing in 1.4.
*/
private Object readResolve() {
return (list instanceof RandomAccess
? new SynchronizedRandomAccessList<>(list)
: this);
}
}
由上述源码可知,在其关键的get、set、remove等方法上,都有关键字synchronized 修饰,因此是线程安全的。
这种方案的缺陷:
与方案一类似,效率较低。
2.4.2 解决方案三
将上述代码:List<String> lists = new ArrayList<>();
改为:List<String> lists = new CopyOnWriteArrayList<>();
举一反三(小猪都会了),直接来看CopyOnWriteArrayList种add方法的源码:
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
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();
}
}
final void setArray(Object[] a) {
array = a;
}
由上述add方法可知,执行add方法时,首先会加lock锁,再将原数组中的元素copy一份,然后在新copy的数组上将元素添加进去,最后切换引用,将新数组赋给底层元数组。这是一种典型的读写分离策略,在日常生产中,大部分场景都是读多写少,CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。所以synchronized保证线程安全,里面的数据安全则交由volatile。
这种方案的优势在于:
读操作效率很高,因为没有任何同步策略,同时由于其读写分离策略,遍历和修改分别在不同的数组容器中,因而不会抛出ConcurrentModificationException异常。
这种方案的缺陷在于:
缺陷在于会占用大量的内存,因为每次读写操作都需要将元数组中元素拷贝一份,数据量大时,会对内存造成较大压力,可能会引起频繁GC;另一点无法保证读写一致性,由于其读写操作是不同的元素组,进行写操作时,可能读取到未写之前的原数组。
2.5 Set案例
2.5.1 关于Set测试
Set与List类似,由于Set与List同属于集合Collection的子类,具体类图如下图所示:
List与Set的主要区别如下:
1. List和Set之间很重要的一个区别是是否允许重复元素的存在,在List中允许插入重复的元素,而在Set中不允许重复元素存在;
2. 与元素先后存放顺序有关,List是有序集合,会保留元素插入时的顺序,Set是无序集合;
3. List可以通过下标来访问,而Set不能。
测试代码类似,如下:
public class SetTest {
public static void main(String[] args) {
HashSet<String> objects = new HashSet<>();
for (int i = 1; i <= 100; i++) {
new Thread(() -> {
objects.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(objects);
}).start();
}
}
}
测试结果也类似,如下:
2.5.2 解决方案
1.方案一
将上述代码中:HashSet<String> objects = new HashSet<>();
修改为:Set<String> objects = Collections.synchronizedSet(new HashSet<>());
原理与synchronizedList类似,可自行查看源码进行分析,这里不再赘述。
2.方案二
将上述代码中:HashSet<String> objects = new HashSet<>();
修改为:Set<String> objects = new CopyOnWriteArraySet<>();
同理可证(小猪都会了),具体可以自行查看源码分析。
3.小结
1.List和Set之间很重要的一个区别是是否允许重复元素的存在,List允许重复元素出现,Set不允许;
2.List和Set都是线程不安全的,通过集合类Collection提供的方法或JUC中提供的方法,能够避免线程不安全问题;
3.每种方案都有利有弊,要根据场景来选择合适的技术,没有银弹。
4.参考文献
1.https://juejin.cn/post/6899330436468899847
2.https://www.cnblogs.com/chengxiao/p/6881974.html
3.https://www.bilibili.com/video/BV1B7411L7tE?p=11&spm_id_from=pageDriver
更多推荐
所有评论(0)