Java ArrayList线程不安全示例说明


class ThreadUnsafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200; 
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}

ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {  
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}

上面的代码抛出

java.lang.IndexOutOfBoundsException: Index: 0, Size: 1

我知道ArrayList不是线程安全的,但在这个例子中,我认为每个remove((调用之前都保证至少有一个add((调用,所以即使顺序混乱,代码也应该是可以的:

thread0: method2()
thread1: method2()
thread1: method3()
thread0: method3() 

这里需要一些解释。

如果总是一个add()remove()调用在另一个调用开始之前完全完成,则您的推理是正确的。但是ArrayList不能保证,因为它的方法不是synchronized。因此,可能会发生两个线程同时处于某些修改调用的中间。

让我们看看add()方法的内部结构,以了解一种可能的故障模式。

添加元素时,ArrayList会使用size++来增加大小。这不是原子的。

现在假设列表为空,两个线程A和B在完全相同的时刻添加一个元素,并行执行size++(可能在不同的CPU内核中(。让我们想象一下事情按以下顺序发生:

  • A将大小读取为0
  • B读取大小为0
  • A的值加1,得到1
  • B将其值加1,得到1
  • A将其新值写回size字段,得到size=1
  • B将其新值写回size字段,从而得到size=1

尽管我们有2个add()调用,但size只有1个。如果现在尝试删除2个元素(这次是按顺序执行的(,则第二个remove()将失败。

为了实现线程安全性,在当前进行一次访问时,任何其他线程都不应该处理像size(或元素数组(这样的内部结构。

多线程本质上是复杂的,因为来自多个线程的调用不仅可以以任何(预期或意外(顺序发生,而且它们也可以重叠,除非受到synchronized等机制的保护。另一方面,过度使用同步很容易导致多线程性能不佳,还会导致死锁。

作为@RalfKleberhoff答案的补充,

我认为每个remove((调用之前都保证至少有一个add((调用,

是。

所以即使订单被搞砸了代码也应该是可以的

否,这不是关于多线程程序的有效推断。

您的程序包含数据竞赛,这是由于两个线程都访问同一个共享的非原子对象,其中一些访问是写入的,而没有适当的同步。包含数据竞赛的程序的整个行为是未定义的,所以实际上你根本无法对它的行为得出任何结论。

不要试图欺骗或节省同步时间。通过限制共享对象的使用,尽量减少你需要的数量,但在哪里需要,你就需要,以及确定何时何地需要的规则并不难学习。

java文档中的ArrayList表示,

请注意,此实现不是同步的。如果有多个线程同时访问ArrayList实例,并且线程从结构上修改了列表,必须对其进行同步外部。

为什么此代码不是线程安全的?

Machine上运行的多个线程彼此独立运行。

public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {  
method2();
method3();
}
}

这里method2()method3()在线程,但不跨线程稳定的状态。

有趣的测试是在method3()中添加空检查并设置LOOP_NUMBER = 10000

private void method3()
{
if (!list.isEmpty())
list.remove(0);
}

结果,您应该得到相同的运行时异常,比如java.lang.IndexOutOfBoundsException: Index: 0, Size: 1java.lang.IndexOutOfBoundsException: Index: 0, Size: 0,因为list中变量(即size(的相同原因不稳定状态。

要解决此问题,您可以添加如下同步或使用同步列表

public void method1(int loopNumber)
{
for (int i = 0; i < loopNumber; i++)
{
synchronized (list)
{
method2();
method3();
}
}
} 

最新更新