Java 中的 ArrayList 和 Multithreading

2022-09-01 18:17:29

在什么情况下,不同步的集合(例如 ArrayList)会导致问题?我想不出任何东西,有人可以给我一个例子,其中ArrayList导致问题,向量解决它?我写了一个有2个线程的程序,两个线程都修改了一个具有一个元素的数组列表。一个线程将“bbb”放入数组列表,而另一个线程将“aaa”放入数组列表。我真的没有看到字符串被修改了一半的实例,我在这里是正确的轨道吗?

另外,我记得我被告知多个线程并没有真正同时运行,1个线程运行了一段时间,另一个线程在那之后运行(在具有单个CPU的计算机上)。如果这是正确的,两个线程怎么可能同时访问相同的数据?也许线程 1 会在修改过程中停止,线程 2 将启动?

提前致谢。


答案 1

有三个方面,如果您使用 ArrayList(例如)而没有充分同步,则可能会出错。

第一种情况是,如果两个线程碰巧同时更新 ArrayList,则它可能会损坏。例如,追加到列表的逻辑是这样的:

public void add(T element) {
    if (!haveSpace(size + 1)) {
        expand(size + 1);
    }
    elements[size] = element;
    // HERE
    size++;
}

现在假设我们有一个处理器/内核和两个线程“同时”在同一列表上执行此代码。假设第一个线程到达标记的点并被抢占。第二个线程出现,并覆盖插槽,因为第一个线程刚刚使用自己的元素更新,然后递增。当第一个线程最终获得控制权时,它会更新 。最终结果是,我们添加了第二个线程的元素,而不是第一个线程的元素,并且很可能还向列表中添加了 。(这只是说明性的。实际上,本机代码编译器可能已对代码进行了重新排序,依此类推。但关键是,如果同时进行更新,可能会发生不好的事情。HEREelementssizesizenull

二种情况是由于 CPU 的缓存内存中主内存内容的缓存而产生的。假设我们有两个线程,一个向列表添加元素,另一个线程读取列表的大小。当 on thread 添加元素时,它将更新列表的属性。但是,由于 不是 ,因此 的新值可能不会立即写出到主内存中。相反,它可以位于高速缓存中,直到 Java 内存模型要求刷新高速缓存的写入的同步点。同时,第二个线程可以调用列表并获取过时值 。在最坏的情况下,第二个线程(例如调用)可能会看到 不一致的 和 数组值,从而导致意外异常。(请注意,即使只有一个内核并且没有内存缓存,也可能发生此类问题。JIT 编译器可以自由地使用 CPU 寄存器来缓存内存内容,并且当发生线程上下文切换时,这些寄存器不会相对于其内存位置进行刷新/刷新。sizesizevolatilesizesize()sizeget(int)sizeelements

三种情况是当您同步 ;例如,将其包装为 .ArrayListSynchronizedList

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

    // Thread 1
    List list2 = ...
    for (Object element : list2) {
        list.add(element);
    }

    // Thread 2
    List list3 = ...
    for (Object element : list) {
        list3.add(element);
    }

如果 thread2 的列表是 or,并且两个线程同时运行,则线程 2 将失败,并显示 .如果是其他(家庭酿造)列表,则结果是不可预测的。问题在于,制作同步列表不足以使其相对于不同线程执行的一系列列表操作的线程安全。为了获得这一点,应用程序通常需要在更高级别/更粗粒度上进行同步。ArrayListLinkedListConcurrentModificationExceptionlist


另外,我记得我被告知多个线程并没有真正同时运行,1个线程运行了一段时间,另一个线程在那之后运行(在具有单个CPU的计算机上)。

正确。如果只有一个内核可用于运行应用程序,则显然一次只能运行一个线程。这使得一些危险变得不可能,而其他危险发生的可能性要小得多。但是,操作系统可以在代码中的任何位置和任何时间从一个线程切换到另一个线程。

如果这是正确的,两个线程怎么可能同时访问相同的数据?也许线程 1 会在修改过程中停止,线程 2 将启动?

是的。这是可能的。它发生的概率非常小1,但这只会使这种问题更加阴险。


1 - 这是因为在硬件时钟周期的时间尺度上测量时,线程时间切片事件非常少见。


答案 2

一个实际的例子。在最终列表中应包含40个项目,但对我来说,它通常显示30到35个项目。猜猜为什么?

static class ListTester implements Runnable {
    private List<Integer> a;

    public ListTester(List<Integer> a) {
        this.a = a;
    }

    public void run() {
        try {
            for (int i = 0; i < 20; ++i) {
                a.add(i);
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
        }
    }
}


public static void main(String[] args) throws Exception {
    ArrayList<Integer> a = new ArrayList<Integer>();

    Thread t1 = new Thread(new ListTester(a));
    Thread t2 = new Thread(new ListTester(a));

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

编辑
周围有更全面的解释(例如,Stephen C的帖子),但是自从mfukar问起,我会做一些评论。(发布答案时应该立即完成)

这是从两个不同线程递增整数的著名问题。在 Sun 关于并发性的 Java 教程中有一个很好的解释。只有在这个例子中,他们有,我们有两次。(是实现的一部分。--i++i++size++sizeArrayList#add