【发布时间】:2011-02-12 13:05:21
【问题描述】:
我们有多个线程在 ArrayList 上调用 add(obj)。
我的理论是,当add 被两个线程同时调用时,只有被添加的两个对象中的一个真正添加到ArrayList。这合理吗?
如果是这样,你如何解决这个问题?使用像 Vector 这样的同步集合?
【问题讨论】:
标签: java concurrency synchronization arraylist
我们有多个线程在 ArrayList 上调用 add(obj)。
我的理论是,当add 被两个线程同时调用时,只有被添加的两个对象中的一个真正添加到ArrayList。这合理吗?
如果是这样,你如何解决这个问题?使用像 Vector 这样的同步集合?
【问题讨论】:
标签: java concurrency synchronization arraylist
当 ArrayList 上的两个线程同时调用 add 时会发生什么,并不能保证行为。但是,根据我的经验,这两个对象都被很好地添加了。大多数与列表相关的线程安全问题都在添加/删除时处理迭代。尽管如此,我强烈建议不要使用具有多线程和并发访问的 vanilla ArrayList。
向量曾经是并发列表的标准,但现在标准是使用Collections synchronized list。
如果您打算花时间在 Java 中处理线程,我强烈推荐 Goetz 等人的 Java Concurrency in Practice。这本书更详细地讨论了这个问题。
【讨论】:
任何事情都可能发生。您可以正确添加两个对象。您只能添加其中一个对象。您可能会收到 ArrayIndexOutOfBounds 异常,因为未正确调整底层数组的大小。或者可能会发生其他事情。可以说你不能依赖任何发生的行为。
作为替代方案,您可以使用Vector、Collections.synchronizedList、CopyOnWriteArrayList,或者您可以使用单独的锁。这完全取决于您还在做什么以及您对集合的访问权限具有何种控制权。
【讨论】:
您还可以获得null、ArrayOutOfBoundsException 或留给实施的其他内容。观察到HashMaps 在生产系统中进入无限循环。你真的不需要知道可能会出什么问题,只是不要这样做。
你可以使用Vector,但它往往会导致界面不够丰富。您可能会发现在大多数情况下您需要不同的数据结构。
【讨论】:
add() 时都会添加null。
我想出了以下代码来模拟真实世界的场景。
100 个任务并行运行,它们将完成状态更新到主程序。我使用 CountDownLatch 等待任务完成。
import java.util.concurrent.*;
import java.util.*;
public class Runner {
// Should be replaced with Collections.synchronizedList(new ArrayList<Integer>())
public List<Integer> completed = new ArrayList<Integer>();
/**
* @param args
*/
public static void main(String[] args) {
Runner r = new Runner();
ExecutorService exe = Executors.newFixedThreadPool(30);
int tasks = 100;
CountDownLatch latch = new CountDownLatch(tasks);
for (int i = 0; i < tasks; i++) {
exe.submit(r.new Task(i, latch));
}
try {
latch.await();
System.out.println("Summary:");
System.out.println("Number of tasks completed: "
+ r.completed.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
exe.shutdown();
}
class Task implements Runnable {
private int id;
private CountDownLatch latch;
public Task(int id, CountDownLatch latch) {
this.id = id;
this.latch = latch;
}
public void run() {
Random r = new Random();
try {
Thread.sleep(r.nextInt(5000)); //Actual work of the task
} catch (InterruptedException e) {
e.printStackTrace();
}
completed.add(id);
latch.countDown();
}
}
}
当我运行应用程序 10 次并且至少运行 3 到 4 次时,程序没有打印正确数量的已完成任务。理想情况下,它应该打印 100(如果没有异常发生)。但在某些情况下,它会打印 98、99 等。
因此证明了 ArrayList 的并发更新不会给出正确的结果。
如果我用 Synchronized 版本替换 ArrayList,程序会输出正确的结果。
【讨论】:
如果你想要arrayList 的线程安全版本,你可以使用List l = Collections.synchronizedList(new ArrayList());。
【讨论】:
这种行为可能是未定义的,因为 ArrayList 不是线程安全的。如果您在迭代器对其进行交互时修改列表,那么您将获得 ConcurrentModificationException。您可以使用 Collection.synchronizedList 包装 ArrayList 或使用线程安全的集合(有很多),或者只是将 add 调用放在同步块中。
【讨论】:
您可以使用 ArrayList(); 代替:
Collections.synchronizedList( new ArrayList() );
或
new Vector();
synchronizedList 对我来说更可取,因为它是:
【讨论】:
java.util.concurrent 有一个线程安全的数组列表。标准的 ArrayList 不是线程安全的,多个线程同时更新时的行为是未定义的。当一个或多个线程同时写入时,多个读取器也会出现奇怪的行为。
【讨论】:
http://java.sun.com/j2se/1.4.2/docs/api/java/util/ArrayList.html
请注意,此实现不同步。如果多个线程同时访问一个 ArrayList 实例,并且至少有一个线程在结构上修改了列表,则必须对外同步。
由于内部没有同步,你的理论是不合理的。
因此,事情变得不同步,结果令人不快且无法预测。
【讨论】:
ConcurrentModificationException 会被抛出,这对我们来说不会发生......也许我们遇到了不使用线程安全集合的另一个副作用......