Java中的ArrayList和多线程

use*_*500 23 java multithreading arraylist

在什么情况下,一个不同步的集合,比如一个ArrayList,会导致问题?我想不出任何问题,有人可以给我一个例子,其中ArrayList导致问题而Vector解决了吗?我写了一个程序,有2个线程都修改了一个元素的arraylist.一个线程将"bbb"放入arraylist,而另一个线程将"aaa"放入arraylist.我真的没有看到字符串被修改一半的实例,我在这里是正确的轨道?

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

提前谢谢了.

Ste*_*n C 29

如果在没有充分同步的情况下使用ArrayList(例如),可能会出现三个方面.

一种情况是,如果两个线程同时更新ArrayList,那么它可能会被破坏.例如,附加到列表的逻辑如下:

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

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

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

第三种情形,当你对操作进行同步出现ArrayList; 例如,将其包裹为SynchronizedList.

    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);
    }
Run Code Online (Sandbox Code Playgroud)

如果thread2的列表是一个ArrayListLinkedList两个线程同时运行,则线程2将失败并带有ConcurrentModificationException.如果是其他(家庭酿造)列表,那么结果是不可预测的.问题在于,使得list同步列表对于由不同线程执行的一系列列表操作而言使其线程安全是不充分的.为此,应用程序通常需要以更高级别/更粗糙的粒度进行同步.


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

正确.如果只有一个核心可用于运行应用程序,那么显然一次只能运行一个线程.这使得一些危险变得不可能,而另一些则不太可能发生.但是,操作系统可以在代码中的任何位置随时从一个线程切换到另一个线程.

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

对.那是可能的.它发生的可能性非常小1,但这只会使这种问题变得更加阴险.


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


Nik*_*bak 21

一个实际的例子.在结尾列表应该包含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));
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑
有更全面的解释(例如,斯蒂芬C的帖子),但我会发表一点评论,因为mfukar问.(应该在发布答案时立即完成)

这是从两个不同线程递增整数的着名问题.在Sun的Java兼容并发教程中有一个很好的解释.只有在那个例子中,他们才有--i,++i而且我们有++size两次.(++sizeArrayList#add实施的一部分.)


Tom*_*Tom 2

什么时候会惹出麻烦呢?

任何时候一个线程正在读取 ArrayList 而另一个线程正在写入,或者当它们都在写入时。这是一个非常著名的例子。

另外,我记得有人告诉我,多个线程并不是真正同时运行,一个线程运行一段时间,然后另一个线程运行(在具有单个 CPU 的计算机上)。如果这是正确的,那么两个线程如何同时访问相同的数据?也许线程 1 会在修改某些内容的过程中停止,而线程 2 会启动?

是的,单核 cpu 一次只能执行一条指令(实际上并非如此,流水线技术已经存在一段时间了,但正如一位教授曾经说过的那样,这是“自由”并行性)。尽管如此,计算机中运行的每个进程只执行一段时间,然后就进入空闲状态。此时,另一个进程可能会开始/继续执行。然后进入空闲状态或完成。进程执行是交错的。

对于线程,也会发生同样的事情,只是它们包含在进程内。它们的执行方式取决于操作系统,但概念保持不变。他们在一生中不断地从活跃变为闲置。