ArrayList为何线程不安全,如何解决

我们知道ArrayList是线程不安全的,与之对应的线程安全Vector,为何?看源码
ArrayList:

    public boolean add(E e) { 
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //ensureCapacityInternal()这个方法的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容
        elementData[size++] = e;
        return true;
    }

Vector:

 public synchronized void addElement(E obj) { 
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

一目了然:Vector的add方法加了synchronized ,而ArrayList没有,所以ArrayList线程不安全,但是,由于Vector加了synchronized ,变成了串行,所以效率低。
不安全详细解释:

1.假设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变为107.线程B也开始进行设置值操作,它尝试设置elementData[10] = e,
  而elementData没有进行过扩容,它的下标最大为9。
  于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。因为他不是一个原子操作,它由如下两步操作构成:
1.elementData[size] = e;
2.size = size + 1;
在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

1.列表大小为0,即size=0
2.线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
3.接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
4.线程A开始将size的值增加为1
5.线程B开始将size的值增加为2

这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。

解决方案:
1.使用 vector代替ArrayList(不建议)
2.使用Collections提供的方法synchronizedList来保证list是同步线程安全(也不建议)

List<String> list = 
Collections.synchronizedList(new ArrayList<>());

《ArrayList为何线程不安全,如何解决》
此图也说明:Set、Map、List类也是线程不安全
3.使用基于写时复制的CopyOnWriteArrayList

拓展:

List->CopyOnWriteArrayList
Set->CopyOnWriteArraySet
Map->concurrentHashmap 注意:synchronizedMap是表锁,效率低,concurrentHashmap,行锁(只锁写入模块),效率高
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable { 
    //写时需要加锁
    final transient ReentrantLock lock = new ReentrantLock();
    //在修改之后需要保证其他读线程能立刻读到新数据
    private transient volatile Object[] array;
    final Object[] getArray() { 
        return array;
    }
    final void setArray(Object[] a) { 
        array = a;
    }
    //增加元素时需要加锁
    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 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据。

  • 优点:
    1.解决的开发工作中的多线程的并发问题。
  • 缺点:
    1.内存占有问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
    2.数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
    参考文章1
    参考文章2
    原文作者:cristianoxm
    原文地址: https://blog.csdn.net/cristianoxm/article/details/105510814
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞