【问题标题】:How can I achieve the safe read/write concurrency on an ArrayList?如何在 ArrayList 上实现安全的读/写并发?
【发布时间】:2014-04-03 16:18:46
【问题描述】:

我的应用程序设计如下:

  1. 数据不断地附加到 ArrayList(通过多次写入)
  2. 读取器线程从列表的开头读取数据(保证在受当前写入影响的索引之前)。

是否有线程安全的方式来实现它,以便:

  • 写入器线程在写入时相互阻塞(以避免写入并发问题)

  • 当其他读者阅读时,读者线程不会被阻塞

  • 读取器线程不会被阻塞当写入器线程追加到末尾时

    我在这里做一个假设,对于数组开头的读取器来说,仅仅写入 ArrayList 的末尾是线程安全的

  • 但是,当写入某些操作会使读取线程不安全时,读取线程会被阻塞 - 如果数组在追加期间用完分配的内存,则会重新分配整个数组数据。

    我在这里假设重新分配 ArrayList 的内存对于数组开头的读取器不是线程安全的 - 如果数组增长实际上是通过在末尾添加更多链接数组而不更改原始数组来实现的这可能是一个错误的假设


更新:最终我没有解决这个令人头疼的问题,而是简单地将整个系统重新设计为数组。这样,我只能在创建 - 或者更确切地说,添加 - 新子数组时锁定父数组;并且任何其他锁都在单独的子数组上,无论如何都会被程序中的所有操作原子处理,因此不会受到锁定问题的影响。

【问题讨论】:

  • 注意:CopyOnWriteArrayList 对我不起作用,因为插入与读取相当,因此每次写入时复制都会非常昂贵
  • 您是否尝试过简单地使用 ReadWriteLock 并进行测量?也许它已经足够快了。
  • 看看 Disruptor 库 lmax-exchange.github.io/disruptor 可能是值得的,它为 java 提供了一个循环缓冲区实现,避免在大多数/所有情况下锁定。

标签: java arraylist concurrency thread-safety


【解决方案1】:

如何在 ArrayList 上实现安全的读/写并发?

很难弄清楚列表实际发生了什么。当读者从前面消费时,项目被添加到末尾,但我不知道是多个读者阅读单个项目还是每个读者都消费每个元素?

但无论如何,我会考虑改用BlockingQueue,比如ArrayBlockingQueueArrayList 根本不是为您的用例构建的。我将有一个BlockingQueue 用于工作,然后另一个BlockingQueue 用于结果。同样,这假设列表中的条目被多个线程消耗。

这里的主要问题之一是同步执行两个功能:互斥锁保护和内存同步。 writer 必须锁定,以免发生冲突,但 reader 需要锁定才能看到 writer 对列表的更新。

就阻塞细节而言,由于动态扩展列表所涉及的竞争条件,您的某些要求是不可能的。

我在这里做一个假设,对于数组开头的读取器来说,仅写入 ArrayList 的末尾是线程安全的

这是真的,除非对列表末尾的写入导致列表被重新分配。但是,同样,您需要在列表上进行同步以查看作者对列表所做的任何更新。即使您的ArrayListvolatile,列表中的字段也不是。

我在这里假设重新分配 ArrayList 的内存对于数组开头的读取器不是线程安全的......

不,不是。当涉及到数据数组时,ArrayList 的任何部分都不是线程安全的。如果有任何问题,我建议查看来源。从add(...) 和任何其他更改列表大小的方法调用以下方法。

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
        if (newCapacity < minCapacity)
            newCapacity = minCapacity;
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
}

【讨论】:

    【解决方案2】:

    我不确定你为什么要使用ArrayList。您所描述的是一个queue,一个First-In-First-Out 数据结构。 Java 提供了几个线程安全的Queue 实现,特别是ConcurrentLinkedQueueLinkedBlockingQueue

    ArrayList 基本上不是线程安全的,唯一的方法是将其包装在对 Collections.synchronizedList() 的调用中,这将使对数组的读写访问基本上是单线程的。

    【讨论】:

      【解决方案3】:

      考虑场景:

      Reader R1 获取列表并使用它

      Writer W1 将一个元素附加到列表中,而不需要重新分配数组

      Reader R2 获取列表并使用它。

      问题是:R1和R2看到的列表对象应该不同,还是可以相同?在第一种情况下,每个写入操作都必须创建新对象,尽管底层数组可以相同(这比使用 CopyOnWriteArrayList 更有效)。在第二种情况下,R1 可以看到列表的长度会增长到不可预测的程度,这可能会导致编程错误。

      在这两种情况下,AtomicReference 都可用于存储列表的当前版本。读取器只需从中获取当前列表,写入器使用带有监视器的附加对象来同步更新操作。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-07-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多