例如,如果您在没有充分同步的情况下使用 ArrayList(例如),则可能会在三个方面出现问题。
The 第一个场景问题是,如果两个线程碰巧同时更新 ArrayList,那么它可能会被损坏。例如,追加到列表的逻辑如下所示:
public void add(T element) {
if (!haveSpace(size + 1)) {
expand(size + 1);
}
elements[size] = element;
// HERE
size++;
}
现在假设我们有一个处理器/核心和两个线程“同时”在同一个列表上执行此代码。假设第一个线程到达标记点HERE
并且被抢占。第二个线程出现,并覆盖了中的插槽elements
第一个线程刚刚更新了自己的元素,然后递增size
。当第一个线程最终获得控制权时,它会更新size
。最终结果是我们添加了第二个线程的元素而不是第一个线程的元素,并且很可能还添加了null
到列表中。 (这只是说明性的。实际上,本机代码编译器可能已经对代码进行了重新排序,等等。但重点是,如果更新同时发生,则可能会发生不好的事情。)
The 第二种情况这是由于 CPU 高速缓冲存储器中缓存了主存内容而产生的。假设我们有两个线程,一个将元素添加到列表中,第二个线程读取列表的大小。当线程添加一个元素时,它将更新列表的size
属性。然而,自从size
is not volatile
,新值size
可能不会立即写出到主存储器。相反,它可以位于缓存中,直到达到 Java 内存模型要求刷新缓存写入的同步点。与此同时,第二个线程可以调用size()
在列表中并获得陈旧值size
。在最坏的情况下,第二个线程(调用get(int)
例如)可能会看到不一致的值size
和elements
数组,导致意外的异常。 (请注意,即使只有一个核心并且没有内存缓存,也可能会发生这种问题。JIT 编译器可以自由地使用 CPU 寄存器来缓存内存内容,并且这些寄存器不会根据其内存位置进行刷新/刷新当发生线程上下文切换时。)
The 第三种情况当您同步操作时出现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);
}
如果 thread2 的列表是ArrayList
or LinkedList
并且两个线程同时运行,线程 2 将失败并显示ConcurrentModificationException
。如果是其他(自制)列表,那么结果是不可预测的。问题是使list
同步列表不足以使其线程安全sequence由不同线程执行的列表操作。为此,应用程序通常需要在更高级别/更粗粒度上进行同步。
另外,我记得有人告诉我,多个线程并不是真正同时运行,一个线程运行一段时间,然后另一个线程运行(在具有单个 CPU 的计算机上)。
正确的。如果只有一个核心可用于运行应用程序,则显然一次只能运行一个线程。这使得一些危险不可能发生,而另一些危险则不太可能发生。但是,操作系统可以在代码中的任何点、任何时间从一个线程切换到另一个线程。
如果这是正确的,那么两个线程如何同时访问相同的数据?也许线程 1 会在修改某些内容的过程中停止,而线程 2 会启动?
Yup. That's possible. The probability of it happening is very small1 but that just makes this kind of problem more insidious.
1 - This is because thread time-slicing events are extremely infrequent, when measured on the timescale of hardware clock cycles.