多个线程同时向不同步的ArrayList的对象添加元素可能会导致哪些问题?

Posted

技术标签:

【中文标题】多个线程同时向不同步的ArrayList的对象添加元素可能会导致哪些问题?【英文标题】:What are the possible problems caused by adding elements to unsynchronized ArrayList's object by multiple threads simultaneously? 【发布时间】:2016-11-26 09:58:16 【问题描述】:

多个线程同时向不同步的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);
        
    

【问题讨论】:

因为文档状态“检测到对象的并发修改的方法可能会抛出此异常当这种修改是不允许的。” AFAIK,ArrayList 不会强制执行规则来禁止这样做。是Iterator。如果您想查看结果,请尝试在其他线程添加时从其中一个线程中删除项目,看看结果是否符合预期。现在,所有线程都只是简单地添加到列表中(并在添加后立即打印,这是此测试的最大缺陷),因此您不能指望控制台显示奇怪的结果 由于System.out.println,您的线程实际上有些同步... Re,“尝试进行一些实验......但找不到太多。”这就是并发错误如此阴险的原因:它们有时很难重现。有时,允许线程对共享数据进行非同步访问的程序可以在几个月的测试中幸存下来,但在发布后只会在客户站点崩溃。 (别问我怎么知道的!) Re, "...a static ArrayList..." 小心你的措辞。 Java 中没有静态 object 这样的东西。在您的示例中,static 是变量,而不是列表。但更重要的是,在您的示例中,重要 是列表由多个线程共享​​>。您不需要 static 变量来共享数据:这只是一种简单的方法。 @assylias 你给了我另一个视角。谢谢 【参考方案1】:

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

发生这种情况是因为 ArrayList 类的以下代码。

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

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

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

这里是上面相同的代码。

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

    运行两三次后,您将在索引 10 的某个时间和 16 的某个时间获得空值。

    如上面 Noscreenname 回答中所述,您可能会从此代码中获得 ArrayIndexOutOfBoundsException。如果删除 Thread.sleep(2) 会频繁生成。

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

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

【讨论】:

感谢您的再次确认。我实际上处于这样一种情况,即第 3 方库懒惰地初始化一个数组,然后偶尔包含空值。我怀疑初始化代码有时是由两个不同的线程触发的,这个答案加深了我的怀疑。谢谢。【参考方案2】:

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

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

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

这是产生这种行为的代码

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());
        
    );

【讨论】:

谢谢@noscreen 我差点忘了。【参考方案3】:

ConcurrentModificationException 仅在使用迭代器迭代同一列表时修改列表时发生。在这里,您只是从不会产生异常的多个线程中将数据添加到您的列表中。尝试在某些地方使用迭代器,您会看到异常。

在下面找到您的示例已修改为产生 ConcurrentModificationException。

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();
        
        Iterator<Integer> itr = list.iterator();
        while(itr.hasNext()) 
            System.out.println("List data : " +itr.next());
        
       

【讨论】:

【参考方案4】:

这是一个简单的例子:我从 10 个线程中将 1000 个项目添加到一个列表中。您希望最后有 10,000 个项目,但您可能不会。如果你多次运行它,你可能每次都会得到不同的结果。

如果您需要 ConcurrentModificationException,您可以在创建任务的循环之后添加 for (Integer i : list)

public static void main(String[] args) throws Exception 
   ExecutorService executor = Executors.newFixedThreadPool(10);
   List<Integer> list = new ArrayList<> ();
   for (int i = 0; i < 10; i++) 
     executor.submit(new ListAdder(list, 1000));
   
   executor.shutdown();
   executor.awaitTermination(1, TimeUnit.SECONDS);

   System.out.println(list.size());


private static class ListAdder implements Runnable 
  private final List<Integer> list;
  private final int iterations;

  public ListAdder(List<Integer> list, int iterations) 
    this.list = list;
    this.iterations = iterations;
  

  @Override
  public void run() 
    for (int i = 0; i < iterations; i++) 
      list.add(0);
    
  

【讨论】:

以上是关于多个线程同时向不同步的ArrayList的对象添加元素可能会导致哪些问题?的主要内容,如果未能解决你的问题,请参考以下文章

多线程环境下需要 ArrayList 的防呆同步

ArrayList,LinkedList,Vector,Set,Map,List,HashMap,HashTable

Java学习--线程

线程安全

Java线程同步synchronized方法与方法块

Java和同步两个线程