通过多个线程同时向未同步的ArrayList的对象添加元素可能引起的问题是什么?

通过多个线程同时向未同步的ArrayList的对象添加元素可能引起的问题是什么?

尝试使用具有多个线程的静态 ArrayList 运行一些实验,但找不到太多。

在这里,我预计在多线程环境中不同步ArrayList或类似对象的许多副作用。

任何显示副作用的好例子都是可观的。谢谢。

以下是我的小实验,运行顺利,没有任何例外。

我也想知道为什么它没有抛出任何 ConcurrentModificationException

import java.util.ArrayList;
import java.util.List;

public class Experiment {
     static List<Integer> list = new ArrayList<Integer>();
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println("A " + i);
            new Thread(new Worker(list, "" + i)).start();
        }
    }   
}

class Worker implements Runnable {
    List<Integer> al;
    String name;

    public Worker(List<Integer> list, String name) {
        this.al = list;
        this.name = name;
    }

    @Override
    public void run() {
        while (true) {
            int no = (int) (Math.random() * 10);
            System.out.println("[thread " + name + "]Adding:" + no + "to Object id:" + System.identityHashCode(al));
            al.add(no);
        }
    }
}

答案 1

调整列表大小以容纳更多元素时,通常会遇到问题。看实现ArrayList.add()

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

如果没有同步,数组的大小将在调用 to 和实际元素插入之间变化。这最终将导致 抛出 。ensureCapacityInternalArrayIndexOutOfBoundsException

下面是生成此行为的代码

final ExecutorService exec = Executors.newFixedThreadPool(8);
final List<Integer> list = new ArrayList<>();
for (int i = 0; i < 8; i++) {
    exec.execute(() -> {
        Random r = new Random();
        while (true) {
            list.add(r.nextInt());
        }
    });
}

答案 2

通过将元素添加到多线程使用的非同步 ArrayList 中,您可以根据需要获得空值来代替实际值。

发生这种情况是由于 ArrayList 类的以下代码。

 public boolean add(E e) {
        ensureCapacity(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
     }

ArrayList类首先检查其当前容量,如果需要,则增加其容量(默认容量为10,下一个增量为(10 * 3)/2),并将默认类级别值放在新空间中。

假设我们使用两个线程,两个线程同时来添加一个元素,发现默认容量(10)已满,并且是时候增加其容量了。在第一个线程1,使用sureCapacity方法(10 +(10 * 3 / 2))以默认值增加ArrayList的大小,并将其元素放在下一个索引(size = 10 + 1 = 11),现在新大小为11。现在第二个线程出现,并使用sureCapacity方法(10 + (10 * 3 / 2))再次使用默认值增加相同ArrayList的大小,并将其元素放在下一个索引(size = 11 + 1 = 12),现在新大小为12。在这种情况下,您将在索引 10 处获得 null,这是默认值。

这是上面的相同代码。

package com;

import java.util.ArrayList;
import java.util.List;

public class Test implements Runnable {

    static List<Integer> ls = new ArrayList<Integer>();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Test());
        Thread t2 = new Thread(new Test());

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(ls.size());
        for (int i = 0; i < ls.size(); ++i) {
            System.out.println(i + "  " + ls.get(i));
        }
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < 20; ++i) {
                ls.add(i);
                Thread.sleep(2);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出:

39
0  0
1  0
2  1
3  1
4  2
5  2
6  3
7  3
8  4
9  4
10  null
11  5
12  6
13  6
14  7
15  7
16  8
17  9
18  9
19  10
20  10
21  11
22  11
23  12
24  12
25  13
26  13
27  14
28  14
29  15
30  15
31  16
32  16
33  17
34  17
35  18
36  18
37  19
38  19
  1. 运行两到三次后,您将在索引 10 处的某个时间获得 null 值,在索引 16 处获得某个时间。

  2. 如上面提到的 答案 by noscreenname 你可以从这个代码中得到 ArrayIndexOutOfBoundsException。如果你去掉 Thread.sleep(2),它就会频繁生成。

  3. 请检查数组的总大小是否小于您的要求。根据代码,它应该是40(20 * 2),但你每次都会得到不同的。

注意:您可能需要多次运行此代码才能生成一个或多个方案。