ArrayList为何线程不安全
JAVA8以前的我就不管了,我手上只有JAVA8以上的环境;
文末有小技巧,如何获得线程安全又高效的list
顺便链接下:HashMap为何线程不安全
对于容器而言,有以下几个动作:增、删、改、查、扩容;
ArrayList的几个动作:增、删、查、扩容;
我们知道ArrayList是线程不安全的,测试代码(使用并发流产生并发)如下:
try {
List<Integer> list1 = new ArrayList();
List<Integer> list2 = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
list1.add(i);
}
list1.parallelStream().forEach(
i -> {
try {
list2.add(i);
}catch (Exception e){
System.out.println(e);
}
}
);
System.out.println("size of list2:" + list2.size());
} catch (Exception e) {
System.out.println(e);
}
java.lang.ArrayIndexOutOfBoundsException: 549
size of list2:959
我们可以看到,list2
不仅数量没有数量没有对
还时常伴随着java.lang.ArrayIndexOutOfBoundsException
即使我们初始化好容量
try {
List<Integer> list1 = new ArrayList();
List<Integer> list2 = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
list1.add(i);
}
list1.parallelStream().forEach(
i -> {
try {
list2.add(i);
}catch (Exception e){
System.out.println(e);
}
}
);
System.out.println("size of list2:" + list2.size());
} catch (Exception e) {
System.out.println(e);
}
size of list2:983
确实好一点,起码不报异常了,数量也更多了,但还是错了;
上面两个测试起码说明了几点:
1.扩容的时候线程不安全,搞不好会搞出异常来;
2.新增动作也是不安全的,
3.删和查想来肯定也不是线程安全的;
我们来看看源代码,各个动作都干了啥;
ArrayList<Integer> list2 = new ArrayList<>();
首先看初始化:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
ArrayList是基于数组的,所以内部有个transient Object[] elementData;
不指定容量的时候,JAVA8中直接初始化了一个空数组
this.elementData = EMPTY_ELEMENTDATA;
初始化到此结束,来看看add
的时候干了啥:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
先不管ensureCapacityInternal
看elementData[size++] = e;
就是个很不安全的动作,因为private int size;
并不是线程安全的,所以比如现在list中有900个元素,list1和list2完全有可能再增加的时候都往901个里写,那就GG了;
我们再来看下扩容:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
恩,看到这我都不想往下看了;
总的来说,读容量,对容量进行增长时,各个变量都没有线程安全,都容易发生并发问题,所以这里多线程出问题的地方就多了;
这是一个彻彻底底不安全的容器;
读我就不写了,总之这个容器类压根没线程安全
要想安全地使用ArrayList,只有一个办法:
请在单线程中使用ArrayList
但是经常需要使用到类似List容器,那么这里有个小技巧
如果你要放的是基础变量且恰巧不重复:
Set<Integer> list2 = ConcurrentHashMap.newKeySet();
是你的最佳选择;
其他小技巧待补充.....用ConcurrentHashMap
来绕一下逻辑是比较推荐的