ArrayList为何线程不安全

2019-01-22  本文已影响0人  手打丸子

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来绕一下逻辑是比较推荐的

上一篇下一篇

猜你喜欢

热点阅读