ArryList线程安全问题以及解决方案

arrayList是一个线程不安全的集合,在多线程情况下可能会引起数据不一致、数组越界等问题。下面具体列一下多线程情况下ArrayList会出现什么错误.

1.java.util.ConcurrentModificationException

 ConcurrentModificationException 中文意思就是并发修改异常,存在于并发使用 Iterator 时出现的时候.

在多线程的情况下,一个线程在使用Iterator进行遍历,另一个线程往集合里面新增了一条数据,就有可能会出现这个错误.

原因:ArrayList中存放了一个属性modCount记录了集合被修改的次数,在创建迭代器(Iterator)时会将modCount赋值到迭代器的expectedModCount属性中,迭代器调用next()方法时判断modCoun是否等于expectedModCount,如果不等于则抛出ConcurrentModificationException异常。

迭代器next方法源码:

public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

判断的方法是:checkForComodification

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

可以看出checkForComodification方法校验了两个属性是否相等,不相等会抛出异常。

多线程情况下出现创建完迭代器后,其他线程往集合里面添加了数据modCount加1,导致modCount与expectedModCount不相等,导致报错。

2.出现NULL问题和元素值覆盖

多线程情况下,会发现调用add方法没有传null的情况,但是输出集合内容时发现存在null值。

ArrayList list = new ArrayList<>();
        for (int i = 0; i < 300; i++) {
            new Thread(() -> {
                if (list.isEmpty()) {
                    list.add(UUID.randomUUID().toString().substring(0, 8));
                    System.out.println(list);
                    list.iterator();

                }
            }, i + "").start();

        }

ArryList线程安全问题以及解决方案_第1张图片

这个问题是add方法引起的问题。

看下add方法的源码:

    public boolean add(E e) {
        //扩容
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //赋值
        elementData[size++] = e;
        return true;
    }

在网上搜索了一下相关问题,有的说是应为扩容所导致的,有的说是赋值使用size++导致的,经过分析个人任务是使用size++导致的。因为size++是非原子性的,多线程情况下会出现问题。

elementData[size++] = e;可为两步

1.给索引位置赋值

2.size加1

如果现在有两个线程a,b,size为5,a线程读到索引位值时,将值赋值到这个位置,这时cpu时间片让出,b线程读到的size也是5(因为a线程没有执行加1操作就将时间片让出来了),b线程将值赋值到size=5这个位置,然后size加1。这个时候a线程恢复cpu时间片,size再加1。这样,a线程和b线程都赋值到了size=5这个位置,索引位置值变成了a线程赋值的值,b线程的值被覆盖了,但是size却加了2次,最后size经过两次加1变成了7,size=6这个这个下标的值就会为null.

ArrayList底层的数据结构是数组,数组里面对应下标没有值就是null.

举例:

        Integer[] elementData = new Integer[22];
        int size = 0;
        for (int i = 0; i< 10; i++) {
            elementData[size] = i;
            size = size + 2;
        }
        System.out.println(Arrays.toString(elementData));

ArryList线程安全问题以及解决方案_第2张图片
 

3.数组下标越界问题

数组越界异常 ArrayIndexOutOfBoundsException

由于ArrayList添加元素是如上面分两步进行,可以看出第一个不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
  5. 线程B也发现需求大小为10,也可以容纳,返回。
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

你可能感兴趣的:(java,开发语言)