高并发下CopyOnWriteArrayList.add(index,element)方法的坑

170 阅读3分钟

大家好,今天跟大家分享下工作中遇到一个高并发下使用CopyOnWriteArrayList遇到的坑,具体的业务就不展开说了,我这里就模拟下,其实主要是为了保证前端传过来的数据在并发处理下能保证一个顺序性

CopyOnWriteArrayList是线程安全的类,那是因为在它的底层,几乎每个方法都进行了加锁

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 static void main(String[] args) throws Exception{

        ThreadPoolExecutor executorMain = new ThreadPoolExecutor(5,5,2, TimeUnit.MINUTES,new LinkedBlockingDeque<>());
        CopyOnWriteArrayList<Integer> tiktokMainImage = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executorMain.execute(()->{
                try{
                    System.out.println(finalI);
                    tiktokMainImage.add(finalI,finalI);
                    System.out.println("size= " + tiktokMainImage.size());
                }catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
            });
        }

        executorMain.shutdown();
        executorMain.awaitTermination(10,TimeUnit.MINUTES);

        Thread.sleep(5000);

        System.out.println("===========================================");
        for (int i = 0; i < 10; i++) {
            System.out.println(tiktokMainImage.get(i));
        }
    }

image.png

发现竟然报了索引越界的问题了,这是怎么回事呢? 我们看下add源码是怎么实现的

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        
        //我们发现报错的日志就是在这里
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
      
    } finally {
        lock.unlock();
    }
}

这里有个判断,就是你传递的下标索引是不是大于当前集合的长度,我们使用线程池来模拟并发情况,所以先获取这把锁Lock的线程是随机的

image.png

CopyOnWriteArrayList初始化的时候,集合的长度默认是0

我们案例是从0到9循环的,如果这时候index是9先获取到,然后执行add(9,"value"),那么此时这个判断

// index = 9, len = 0
// 所以这时候就直接抛出异常了
if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);

好了,这时候我们知道是怎么回事了,那么我们在给CopyOnWriteArrayList初始化的时候,先设置好集合长度不就可以了嘛,现在把我们的案例修改下

我们现在给CopyOnWriteArrayList先设置10个值,然后在add的时候修改对应索引下标的值

public static void main(String[] args) throws Exception{

    ThreadPoolExecutor executorMain = new ThreadPoolExecutor(5,5,2, TimeUnit.MINUTES,new LinkedBlockingDeque<>());

    // 我们现在给CopyOnWriteArrayList先设置10个值,然后在add的时候修改索引的值
    List<Integer> list = new ArrayList<>();
    for(int i = 0; i < 10; i ++) {
        list.add(i);
    }
    CopyOnWriteArrayList<Integer> tiktokMainImage = new CopyOnWriteArrayList<>(list);
    for (int i = 0; i < 10; i++) {
        int finalI = i;
        executorMain.execute(()->{
            try{
                tiktokMainImage.add(finalI,finalI);
            }catch (Exception e) {
                e.printStackTrace();
                return;
            }
        });
    }

    executorMain.shutdown();
    executorMain.awaitTermination(10,TimeUnit.MINUTES);

    Thread.sleep(5000);

    System.out.println("===========================================");
    for (int i = 0; i < 10; i++) {
        System.out.println(tiktokMainImage.get(i));
    }
}

image.png

我们发现现在不报数组越界的异常了,但是仔细看,我们发现竟然有2个0,但是4这个元素在add修改的时候失败了

这里我直接给结论,把我们的代码修改如下

我们不使用add方法,改成使用set方法

public static void main(String[] args) throws Exception{

    ThreadPoolExecutor executorMain = new ThreadPoolExecutor(5,5,2, TimeUnit.MINUTES,new LinkedBlockingDeque<>());

    List<Integer> list = new ArrayList<>();
    for(int i = 0; i < 10; i ++) {
        list.add(i + 100);
    }
    CopyOnWriteArrayList<Integer> tiktokMainImage = new CopyOnWriteArrayList<>(list);
    for (int i = 0; i < 10; i++) {
        int finalI = i;
        executorMain.execute(()->{
            try{
                tiktokMainImage.set(finalI,finalI);
            }catch (Exception e) {
                e.printStackTrace();
                return;
            }
        });
    }

    executorMain.shutdown();
    executorMain.awaitTermination(10,TimeUnit.MINUTES);

    Thread.sleep(5000);

    System.out.println("===========================================");
    for (int i = 0; i < 10; i++) {
        System.out.println(tiktokMainImage.get(i));
    }
}

image.png

我们发现现在结果就对了,大家可以多试几次,结果都是正确的

至于为什么会这样,我这就去研究下,所以大家在并发情况下使用到CopyOnWriteArrayList的add方法去修改原有的值,那就使用set,不要使用add方法了