ArrayList为什么是线程不安全的?

目录

前言

一、ArrayList扩容

一、添加元素流程

二、是扩容当前容量的一半还是所需的最小容量?

二、ArrayList为什么是线程不安全的?

三、ArrayList高并发下会出现什么问题?

四、线程安全的ArrayList

总结


前言

ArrayList是使用较为频繁的一种数据结构,其底层是基于动态数组实现,具有快速随机访问的优点。而ArrayList添加元素以及扩容并不是原子的,因此在高并发的情况下并不能保证数据的正确性和一致性。(以下代码基于JDK23版本,与JDK1.8版本部分不一致,是因为JDK10之后扩容机制做了一些调整。)

一、ArrayList扩容

一、添加元素流程

    private static final int DEFAULT_CAPACITY = 10;
    
    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

    private Object[] grow() {
        return grow(size + 1);
    }

    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

从上述源码来看,ArrayList添加元素先判断当前数组的长度是否能够放入,当数组长度足够则直接放入,且size值加1。当数组长度不足以放入新元素,则会进行扩容。扩容时传入的三个参数分别是旧数组的容量、所需的最小容量与旧数组容量的差值以及旧容量的一半。

二、是扩容当前容量的一半还是所需的最小容量?

    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // preconditions not checked because of inlining
        // assert oldLength >= 0
        // assert minGrowth > 0

        int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
            return prefLength;
        } else {
            // put code cold in a separate method
            return hugeLength(oldLength, minGrowth);
        }
    }

扩容长度由所需最小容量与当前容量一半共同决定,取其中的最大值。

二、ArrayList为什么是线程不安全的?

整体上ArrayList添加一个元素的流程为:

  1. 判断是否需要扩容,如果需要则先扩容
  2. 添加元素
  3. size值加1

由于这些操作并不是原子的,在高并发下会受到其他线程的影响,会造成数据混乱问题。

三、ArrayList高并发下会出现什么问题?

1.会造成数据丢失:当线程1在索引为5的位置添加新的元素,由于小于初始容量10不需要扩容,所以直接添加元素,但此时size的值还未加1。线程2进行也进行添加元素,而此时的size依旧为5,会将线程1中添加的element[5]中的元素进行覆盖,造成了数据的丢失。同时,由于会size会进行两次加1,下一次元素则会添加在element[7],所以会造成element[6]中的值为null。

2.索引越界异常:当线程1在size为9的位置添加元素,此时不需要扩容。而线程2此时也开始添加元素,由于线程1的size还未加1,因此线程2依旧是添加在size为9的位置上。最终size加两次后,此时的size为11,下次进行扩容判断时会因为(11 == 10)的条件为false,导致不会进行扩容操作,此时会将元素添加在element[11],造成索引越界异常

3.size值与add的次数不一致:由于size+1的操作不是原子的,该操作可以分为三步:获取size值、size值加1、赋值给size。因此会出现线程1与线程2拿到的是同一个值,加完后再赋值会出现只加1次,而本应该加两次,造成size值与实际不一致的情况。例如此时size值为5,线程1与线程2读取的都是5,两者都对size加1并赋值,此时size值为6,而实际size值需要加2次,也就是size最终结果应该为7。

四、线程安全的ArrayList

因此,当我们需要保证线程安全的情况下,可以使用CopyOnWriteArrayList来代替ArrayList。


总结

本文从源码的角度解释了ArrayList添加元素的流程,以及简述了扩容机制。并分析了高并发下ArrayList添加元素可能会出现数据丢失、索引越界异常、size值与add的次数不一致等问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值