【问题标题】:Java ArrayList thread unsafe example explanationJava ArrayList 线程不安全示例解释
【发布时间】:2021-01-24 09:38:52
【问题描述】:
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() 

这里需要一些解释,请。

【问题讨论】:

  • ArrayList 本身不是线程安全的,因此如果两个线程同时执行 add 或 remove 调用,可能会发生不好的事情。
  • 你能提个问题吗?
  • @Milgo 为什么会抛出 java.lang.IndexOutOfBoundsException?代码执行顺序可能会混乱,但我想不出会发生 IndexOutOfBoundsException 的情况。
  • @handhand 这些操作不是原子的,所以当一个正在进行时,列表的状态没有很好地定义。正如您自己所说,“ArrayList 不是线程安全的”。你认为这意味着什么?如果您想查看到底发生了什么,请在调试器下运行您的程序,并在 IndexOutOfBoundsException 上设置一个断点。如果您添加完整的堆栈跟踪,有人可能会准确地告诉您发生了什么。

标签: java multithreading arraylist


【解决方案1】:

如果一个add()remove() 呼叫总是在另一个呼叫开始之前完全完成,那么您的推理是正确的。但是ArrayList 并不能保证它的方法不是synchronized。因此,可能会发生两个线程同时在进行一些修改调用的情况。

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

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

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

  • A 读取大小为 0。
  • B 读取大小为 0。
  • A 将其值加一,得到 1。
  • B 将其值加一,得到 1。
  • A 将其新值写回size 字段,从而生成size=1
  • B 将其新值写回size 字段,结果为size=1

虽然我们有 2 个 add() 调用,但 size 只有 1 个。如果现在您尝试删除 2 个元素(这一次它是按顺序发生的),第二个 remove() 将失败。

为了实现线程安全,当一个访问正在进行时,任何其他线程都不能像size(或元素数组)那样搞乱内部结构。

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

【讨论】:

    【解决方案2】:

    作为@RalfKleberhoff's answer的补充,

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

    是的。

    所以代码应该没问题,即使订单搞砸了

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

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

    不要试图欺骗或吝啬同步。通过限制对共享对象的使用,尽量减少你需要的数量,但是在你需要它的地方,你需要它,并且确定何时何地需要它的规则并不难学习。

    【讨论】:

      【解决方案3】:

      ArrayList 在 java 文档中说,

      请注意,此实现不同步。如果多个线程 同时访问一个 ArrayList 实例,并且至少其中一个 线程在结构上修改列表,它必须同步 外部。

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

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

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

      这里method2()method3()正在处理在 线程,但不是跨线程。 ArrayList list 在两个线程之间是通用的。这将在多核系统上的两个线程之间处于 inconstant 状态。

      有趣的测试是在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中变量的状态inconstant,即size

      要解决此问题,您可以添加如下所示的同步或使用 Syncronized list

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-10-28
        • 1970-01-01
        • 1970-01-01
        • 2011-02-10
        • 2019-12-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多