Java ArrayList 线程不安全示例说明

han*_*and 4 java multithreading 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);
    }

}
Run Code Online (Sandbox Code Playgroud)

上面的代码抛出

java.lang.IndexOutOfBoundsException: Index: 0, Size: 1
Run Code Online (Sandbox Code Playgroud)

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

thread0: method2()
thread1: method2()
thread1: method3()
thread0: method3() 
Run Code Online (Sandbox Code Playgroud)

这里需要一些解释,请。

Ral*_*off 5

如果总是一个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. 另一方面,过度使用同步很容易导致多线程性能不佳,也会导致死锁。