【问题标题】:Java MultiThreading behaviour explanationJava MultiThreading 行为解释
【发布时间】:2016-02-20 05:04:55
【问题描述】:

我正在学习 Java 多线程。我写了一小段代码并产生了一些我无法理解的输出。请帮忙解释一下。 下面贴出代码。

package com.java.learn;

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

public class ListTestWithMultiThread {
    static final List<Integer> list = new ArrayList<Integer>();

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 100; i++) {
                    list.add(Integer.valueOf(i));
                }
                System.out.println("List size at thread 0 : " + list.size());
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 101; i <= 200; i++) {
                    list.add(Integer.valueOf(i));
                }
                System.out.println("List size at thread 1 : " + list.size());
            }
        }).start();
    }
}

各种运行中的一些 o/p: 线程 0 的列表大小:134 线程 1 的列表大小:200

Exception in thread "Thread-1" List size at thread 0 : 101
java.lang.ArrayIndexOutOfBoundsException: 17
    at java.util.ArrayList.add(Unknown Source)
    at com.java.learn.ListTestWithMultiThread$2.run(ListTestWithMultiThread.java:25)
    at java.lang.Thread.run(Unknown Source)

    List size at thread 0 : 106
Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 58
    at java.util.ArrayList.add(Unknown Source)
    at com.java.learn.ListTestWithMultiThread$2.run(ListTestWithMultiThread.java:25)
    at java.lang.Thread.run(Unknown Source)

【问题讨论】:

    标签: java multithreading indexoutofboundsexception


    【解决方案1】:

    您正在访问的数据结构 (list) 不是为并行并发访问而设计的,但未对其进行保护(例如,通过 synchronized)。这最终会破坏数据结构的内部结构,导致出现异常行为等奇怪行为。

    这里有两种方法来处理它:

    1. 使用并发数据结构:

    List&lt;Integer&gt; list = Collections.synchronizedList(new ArrayList&lt;Integer&gt;());

    1. 使用synchronized 保护列表:

      synchronized(list) {
            list.add(Integer.valueOf(i));
      }
      

    编辑:既然您要求这样做,那么ArrayList 是如何损坏的。 ArrayList 由一个数组支持,该数组在列表增长时必须调整大小。这里调整大小意味着分配一个新的、更大的数组,并将旧数组的内容复制到新数组。以下是 the code 的部分内容:

    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    ...
    elementData = Arrays.copyOf(elementData, newCapacity);
    

    现在想象以下情况:线程 A 开始调整数组大小,它计算新容量并开始复制第 4 行中的数据。但在它可以将新数组的引用复制到 elementData 之前,它会因任何原因停止(这事儿常常发生)。现在线程 B 开始调整数组大小并完成。然后它将更多值插入到列表中并再次调整数组大小并完成。线程 B 现在假定列表足够大以容纳新值,但在插入该值之前,线程 A 唤醒并使用它创建的较小数组的引用覆盖 elementData。线程 B 现在尝试将一个值插入到较小的数组中并获得一个ArrayIndexOutOfBoundsException。这一切都不太可能发生,但正如您所见,它可能会发生。

    【讨论】:

    • 如果我需要使用 syncronized(list){},那么我需要在两个线程或任何一个线程中进行。
    • 在两个线程中 - 基本上无论您以并发方式修改它的任何位置
    • 是否将引用变量列表设为静态对我们得到的行为没有影响。
    • 不,没有任何区别
    • Martin,既然只有一个共享列表对象,两个线程如何在内部创建不同的数组。他们不使用内部创建的相同数组吗?
    【解决方案2】:

    您正在从两个不同的线程访问list 变量,而没有任何同步(锁定)。这将导致未定义的行为。

    您可能想尝试将列表声明替换为以下内容:

    static final List list = Collections.synchronizedList(new ArrayList());
    

    另一种选择是使用Vector 而不是ArrayList。 Vector 是实现List 接口的同步集合。

    【讨论】:

    • 我尝试多次运行代码,输出结果是:线程 0 的列表大小:100 线程 1 的列表大小:200 一直......
    • 即使我将列表设置为静态易失性,它应该直接写入主内存并保证对其他线程的可见性,那么也是相同的行为
    • Vaibhav,我有时也得到了那个 o/p,但不是所有时候。有时会导致异常,如我的 o/p 共享中所示
    • @KamalBindra volatile 跟线程间同步数据关系不大
    • @KamalBindra, volatile 如果这是一个可以从两个线程访问的简单变量(例如,int),将会有所帮助。但是,由于您在更复杂的列表对象上调用方法(并且在内部管理多个数据结构),这还不够。
    【解决方案3】:
    • ArrayList 不是线程安全的。 ArrayList 实现已备份 通过一个数组。有关联的大小变量ArrayList。每当我们 将任何元素添加到 ArrayList 它首先确保它的容量,然后 向数组中添加元素。数组列表的什么状态可见 每个线程未定义,因为您共享非线程安全 两个线程之间的实现。

    • 如果你先启动任何线程,不要假设它会完成它 先做任务。没有预测的行为。

    【讨论】:

    • 感谢大家的回复。任何人都可以帮助我对内存中的列表进行一些图形表示,使其损坏并导致 arrayindexoutofbound 异常。它会帮助我了解更多。
    • @KamalBindra 我在我的回答中添加了一个解释
    【解决方案4】:

    我认为错误是出站而不是 java.util.ConcurrentModificationException 要了解输出,您应该首先了解 arraylist 在内部的工作。当您添加 arraylist 时,内部会发生什么。 ArrayList 的大小如何动态增长?

    在 add(Object) 里面,你会发现下面的代码

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

    从上面的代码中需要注意的重要一点是,在添加元素之前,我们正在检查 ArrayList 的容量。 ensureCapacity() 确定当前占用元素的大小以及数组的最大大小。如果填充元素的大小(包括要添加到 ArrayList 类的新元素)大于数组的最大大小,则增加数组的大小。但是数组的大小不能动态增加。所以内部发生的事情是使用容量创建新数组。

    所以在你的情况下你的 线程 1 和线程 2 尝试并成功同时添加元素,幸运的是没有同步错误。直到 Arraylist 已满,现在两个线程都创建了 2 个不同的新数组。假设它的线程 1,其中一个线程获胜以更改对其新创建的数组的引用。现在线程 2 尝试复制新引用数组的所有元素,不幸的是他不知道数组的大小变化,因此重新计算数组的大小并尝试在不存在的数组顶部。因此抛出异常超出范围。

    如果我理解错了,请纠正我。

    【讨论】:

    • 如果创建的arraylist列表是内存中的单个共享对象并且引用也是静态的,两个线程如何创建两个不同的数组。
    • 基本上arraylist是一个简单的数组,因此arraylist的大小是通过创建新的数组对象并更改其对新数组的引用来动态增加的。因此,当两个线程都看到arraylist已满,然后都创建新数组。
    • 首先将变量列表引用到arraylist 是静态和最终的,这意味着不能更改引用。其次同意arraylist是内部数组,当大小增加时,创建新数组,但新数组对于两个线程来说都是单一的,或者对于两个线程来说都是不同的。如果两个线程的两个不同,如何更改 ref。
    • 我理解您的问题最终意味着您无法将对象分配给该对象意味着无法更改其引用。但这并不意味着 final 关键字适用于内部数组。在内部,它可以根据需要进行修改。因为我们不是整体更改类对象[ArrayList]的引用,而是更改内部数组对象。
    猜你喜欢
    • 1970-01-01
    • 2020-01-20
    • 2011-12-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-05
    • 2019-02-06
    相关资源
    最近更新 更多