【问题标题】:Is there a concurrent List in Java's JDK?Java JDK中是否有并发列表?
【发布时间】:2011-10-18 11:39:31
【问题描述】:

如何创建一个并发 List 实例,我可以在其中按索引访问元素? JDK 有我可以使用的类或工厂方法吗?

【问题讨论】:

  • 为什么不建设性? .Net 中找不到的几个建议的 CopyOnWriteArrayList。您可以说这两个问题相互关联,但不要关闭这个问题!!!
  • 我不知道为什么 Jarrod Roberson 会认为接受 Stephan 所做的详细编辑并将其恢复为最初的措辞不佳的问题是个好主意。 Jarrod 的回答仍然是完全可以接受的。实际上,CopyOnWriteArrayList 是 JDK 中唯一实现 List 的并发类。困惑...
  • 因为接受的答案是原始问题,而斯蒂芬用一堆源代码提出了一个完全不相关的问题,原始海报没有包括 anywhere 完全改变问题,这产生了更多的答案,这些答案暗示了 List 以外的其他东西,原件明确表示这是一个被认为是故意破坏的 要求。版主已经锁定了问题,因为有人抱怨答案没有回答问题的破坏版本。
  • /locked/closed/ 上一条评论
  • 没有理由关闭这个问题。它询问 JDK 中的类,这与搜索库完全不同;它是 Java 的基础。

标签: java list concurrency


【解决方案1】:

免责声明:此答案发布于 2011 年,在 JDK 5 之前,在许多高级和优化的并发 API 之前。因此,虽然以下 有效,但它不是最佳选择。


如果您只需要简单的调用同步,您可以很好地使用Collections.synchronizedList(List)

 List<Object> objList = Collections.synchronizedList(new ArrayList<Object>());

【讨论】:

  • synchronizedList 的结果是“同步”但不是“并发”。一个基本问题是,许多基于索引的 List 操作本身不是原子的,需要成为更大的互斥结构的一部分。
  • IMO,VectorCollections.synchronizedList(new ArrayList&lt;Object&gt;()) 更直接。
  • synchronizedList的结果是“同步”但不是“并发”。
【解决方案2】:

您有以下选择:

  • Collections.synchronizedList():您可以包装任何List 实现(ArrayListLinkedList 或第 3 方列表)。使用synchronized 将保护对每种方法(读取和写入)的访问。当使用iterator()或增强的for循环时,必须手动同步整个迭代。在迭代时,其他线程甚至被完全阻止读取。您也可以分别为每个 hasNextnext 调用进行同步,但随后 ConcurrentModificationException 是可能的。

  • CopyOnWriteArrayList: 修改成本高,但无需等待即可阅读。迭代器从不抛出ConcurrentModificationException,它们在迭代器创建时返回列表的快照,即使列表在迭代时被另一个线程修改。对于不经常更新的列表很有用。像 addAll 这样的批量操作更适合更新 - 内部数组被复制的次数更少。

  • Vector:非常像synchronizedList(new ArrayList&lt;&gt;()),但迭代也是同步的。但是,如果向量在迭代时被另一个线程修改,迭代器可能会抛出 ConcurrentModificationException

其他选项:

  • QueueDeque 如果您仅在列表末尾添加/删除或迭代它,则可能是一种替代方法。 Queue 只允许在一端添加并从另一端移除,Deque 允许在两端添加和移除。没有按索引访问。有多种实现具有比任何List 提供的更好的并发属性。查看Queue javadoc 中的“所有已知实现类”,java.util.concurrent 包中的那些实现是并发的。您还可以查看JCTools,它包含专门针对单个消费者或单个生产者的更快的队列实现。
  • Collections.unmodifiableList():无需等待,线程安全,但不可修改
  • List.of & List.copyOf:Java 9 及更高版本中的另一个 non-modifiable list

【讨论】:

    【解决方案3】:

    ConcurrentLinkedQueue

    如果您不关心基于索引的访问,而只想要 List 的插入顺序保留特性,您可以考虑使用 java.util.concurrent.ConcurrentLinkedQueue。由于它实现了 Iterable,一旦你完成了所有项目的添加,你可以使用增强的 for 语法循环内容:

    Queue<String> globalQueue = new ConcurrentLinkedQueue<String>();
    
    //Multiple threads can safely call globalQueue.add()...
    
    for (String href : globalQueue) {
        //do something with href
    }
    

    【讨论】:

    • 我认为简化的for语句(:)叫做foreach:docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html
    • @AlikElzin-kilaka 你是对的。我认为这个名字一直困扰着我,因为实际的语法不包括“每个”这个词,但我会更新答案以使用官方名称。 :)
    • @AlikElzin-kilaka Nitpicking,但根据JLS version 8,它被称为“增强的声明”。 java tutorial 也是一样。
    • @Roland 绝对不会吹毛求疵。 Java 中的“for each”和“enhanced for”(现在)有区别。
    • @Roland 间接。我相信他们将“for each loop”重命名为“enhanced for”,以消除 Stream.forEach 和现在所谓的增强 for 之间的混淆。
    【解决方案4】:

    大多数情况下,如果您需要一个并发列表,它位于模型对象内部(因为您不应使用列表等抽象数据类型来表示应用程序模型图中的节点)或者它是特定服务的一部分,您可以同步自己访问。

    class MyClass {
      List<MyType> myConcurrentList = new ArrayList<>();
      void myMethod() {
        synchronzied(myConcurrentList) {
          doSomethingWithList;
        }
      }
    }
    

    通常这足以让您继续前进。如果您需要迭代,请迭代列表的副本而不是列表本身,并且仅在迭代列表时同步您复制列表的部分。

    此外,当同时处理一个列表时,您通常会做的不仅仅是添加、删除或复制,这意味着该操作变得足够有意义以保证其自己的方法,并且列表成为一个特殊类的成员,该特殊类仅代表这个特定列表线程安全行为。

    即使我同意需要一个并发列表实现并且 Vector / Collections.sychronizeList(list) 并没有做到这一点,因为可以肯定你需要 compareAndAdd 或 compareAndRemove 或 get(..., ifAbsentDo) 之类的东西,即使你有一个 ConcurrentList 实现,开发人员在处理并发列表(和映射)时经常会引入错误,因为他们没有考虑真正的事务是什么。

    这些事务对于与并发 ADT(抽象数据类型)交互的预期目的而言太小的场景总是导致我将列表隐藏在一个特殊的类中并使用 synchronized 同步访问此类对象方法在方法层面。这是确保交易正确的唯一方法。

    我已经看到了太多的错误,无法以任何其他方式进行 - 至少在代码很重要并且处理金钱或安全之类的事情或保证某些服务质量措施(例如,至少发送一次且仅一次)的情况下。

    【讨论】:

    • 同步锁,因此只有 1 个线程可以访问。并发意味着以最少的锁进行多线程访问。
    • 第一个最小锁是一个任意概念。在同步块中,线程对某个资源具有独占访问权。但是从外部观察多个进程会得出这样的结论,即多个线程/“进程”可以“同时”访问同一资源(并发列表),但以线程安全的方式。例如一个线程一个一个地添加 100 个元素,而另一个线程访问同一个列表并在列表包含 50 个元素时复制它。这称为资源的同时或并发访问,因为两个线程都访问同一资源。
    • 在访问非线程安全资源时让其他线程等待是实现并发的完全有效的方法。我认为这个答案不应该被否决。尤其是在很多情况下,当我不必经常访问一个非常大的列表时,我宁愿访问一个非常快的带有锁定的 ArrayList,而不是 CopyOnWrite,这可能非常昂贵。
    【解决方案5】:

    如果您从不打算从列表中删除元素(因为这需要更改已删除元素之后所有元素的索引),那么您可以使用ConcurrentSkipListMap&lt;Integer, T&gt; 代替ArrayList&lt;T&gt;,例如

    NavigableMap<Integer, T> map = new ConcurrentSkipListMap<>();
    

    这将允许您将项目添加到“列表”的末尾,如下所示,只要只有一个写入线程(否则map.size() 和 @ 之间存在竞争条件987654325@):

    // Add item to end of the "list":
    map.put(map.size(), item);
    

    显然,您也可以通过简单地调用map.put(index, item) 来修改“列表”(即地图)中任何项目的值。

    将项目放入地图或通过索引检索它们的平均成本是 O(log(n)),并且ConcurrentSkipListMap 是无锁的,这比说Vector(旧的同步版本)要好得多ArrayList)。

    您可以使用NavigableMap接口的方法来回遍历“列表”。

    您可以将以上所有内容包装到实现List 接口的类中,只要您了解竞争条件警告(或者您可以只同步编写器方法)——并且您需要抛出一个不受支持的操作remove 方法的例外。实现所有必需的方法需要相当多的样板文件,但这里是一个快速实现的尝试。

    import java.util.Collection;
    import java.util.Iterator;
    import java.util.List;
    import java.util.ListIterator;
    import java.util.NavigableMap;
    import java.util.Objects;
    import java.util.Map.Entry;
    import java.util.concurrent.ConcurrentSkipListMap;
    
    public class ConcurrentAddOnlyList<V> implements List<V> {
        private NavigableMap<Integer, V> map = new ConcurrentSkipListMap<>();
    
        @Override
        public int size() {
            return map.size();
        }
    
        @Override
        public boolean isEmpty() {
            return map.isEmpty();
        }
    
        @Override
        public boolean contains(Object o) {
            return map.values().contains(o);
        }
    
        @Override
        public Iterator<V> iterator() {
            return map.values().iterator();
        }
    
        @Override
        public Object[] toArray() {
            return map.values().toArray();
        }
    
        @Override
        public <T> T[] toArray(T[] a) {
            return map.values().toArray(a);
        }
    
        @Override
        public V get(int index) {
            return map.get(index);
        }
    
        @Override
        public boolean containsAll(Collection<?> c) {
            return map.values().containsAll(c);
        }
    
        @Override
        public int indexOf(Object o) {
            for (Entry<Integer, V> ent : map.entrySet()) {
                if (Objects.equals(ent.getValue(), o)) {
                    return ent.getKey();
                }
            }
            return -1;
        }
    
        @Override
        public int lastIndexOf(Object o) {
            for (Entry<Integer, V> ent : map.descendingMap().entrySet()) {
                if (Objects.equals(ent.getValue(), o)) {
                    return ent.getKey();
                }
            }
            return -1;
        }
    
        @Override
        public ListIterator<V> listIterator(int index) {
            return new ListIterator<V>() {
                private int currIdx = 0;
    
                @Override
                public boolean hasNext() {
                    return currIdx < map.size();
                }
    
                @Override
                public V next() {
                    if (currIdx >= map.size()) {
                        throw new IllegalArgumentException(
                                "next() called at end of list");
                    }
                    return map.get(currIdx++);
                }
    
                @Override
                public boolean hasPrevious() {
                    return currIdx > 0;
                }
    
                @Override
                public V previous() {
                    if (currIdx <= 0) {
                        throw new IllegalArgumentException(
                                "previous() called at beginning of list");
                    }
                    return map.get(--currIdx);
                }
    
                @Override
                public int nextIndex() {
                    return currIdx + 1;
                }
    
                @Override
                public int previousIndex() {
                    return currIdx - 1;
                }
    
                @Override
                public void remove() {
                    throw new UnsupportedOperationException();
                }
    
                @Override
                public void set(V e) {
                    // Might change size of map if currIdx == map.size(),
                    // so need to synchronize 
                    synchronized (map) {
                        map.put(currIdx, e);
                    }
                }
    
                @Override
                public void add(V e) {
                    synchronized (map) {
                        // Insertion is not supported except at end of list
                        if (currIdx < map.size()) {
                            throw new UnsupportedOperationException();
                        }
                        map.put(currIdx++, e);
                    }
                }
            };
        }
    
        @Override
        public ListIterator<V> listIterator() {
            return listIterator(0);
        }
    
        @Override
        public List<V> subList(int fromIndex, int toIndex) {
            // TODO Auto-generated method stub
            return null;
        }
    
        @Override
        public boolean add(V e) {
            synchronized (map) {
                map.put(map.size(), e);
                return true;
            }
        }
    
        @Override
        public boolean addAll(Collection<? extends V> c) {
            synchronized (map) {
                for (V val : c) {
                    add(val);
                }
                return true;
            }
        }
    
        @Override
        public V set(int index, V element) {
            synchronized (map) {
                if (index < 0 || index > map.size()) {
                    throw new IllegalArgumentException("Index out of range");
                }
                return map.put(index, element);
            }
        }
    
        @Override
        public void clear() {
            synchronized (map) {
                map.clear();
            }
        }
    
        @Override
        public synchronized void add(int index, V element) {
            synchronized (map) {
                if (index < map.size()) {
                    // Insertion is not supported except at end of list
                    throw new UnsupportedOperationException();
                } else if (index < 0 || index > map.size()) {
                    throw new IllegalArgumentException("Index out of range");
                }
                // index == map.size()
                add(element);
            }
        }
    
        @Override
        public synchronized boolean addAll(
                int index, Collection<? extends V> c) {
            synchronized (map) {
                if (index < map.size()) {
                    // Insertion is not supported except at end of list
                    throw new UnsupportedOperationException();
                } else if (index < 0 || index > map.size()) {
                    throw new IllegalArgumentException("Index out of range");
                }
                // index == map.size()
                for (V val : c) {
                    add(val);
                }
                return true;
            }
        }
    
        @Override
        public boolean remove(Object o) {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public V remove(int index) {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public boolean removeAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public boolean retainAll(Collection<?> c) {
            throw new UnsupportedOperationException();
        }
    }
    

    不要忘记,即使使用如上所示的编写器线程同步,您也需要小心不要遇到可能导致您删除项目的竞争条件,例如,如果您尝试遍历阅读器中的列表线程同时写入线程正在添加到列表的末尾。

    你甚至可以使用ConcurrentSkipListMap作为一个双端列表,只要你不需要每个项目的键来代表列表中的实际位置(即添加到列表的开头会分配项目否定键)。 (同样的竞争条件警告在这里适用,即应该只有一个写入线程。)

    // Add item after last item in the "list":
    map.put(map.isEmpty() ? 0 : map.lastKey() + 1, item);
    
    // Add item before first item in the "list":
    map.put(map.isEmpty() ? 0 : map.firstKey() - 1, item);
    

    【讨论】:

      【解决方案6】:

      java.util.concurrent 中有一个并发列表实现。尤其是CopyOnWriteArrayList

      【讨论】:

      • 请注意,它会在每次插入时复制整个列表,因此通常效率低下。
      • @dfrankow 但如果您的迭代次数远多于更新次数,它会更高效。
      • 无法正常工作,如此处所示。即使我只是使用它的 addAll 方法并使用流读取它,我也有例外。 stackoverflow.com/questions/1527519/…
      【解决方案7】:

      CopyOnWriteArrayList 是 ArrayList 的线程安全变体,其中 所有可变操作(添加、设置等)都由 制作底层数组的新副本。

      CopyOnWriteArrayList 是同步 List 的并发替代方案,实现 List 接口及其 java.util.concurrent 包的一部分,它是一个线程安全的集合。

      public class CopyOnWriteArrayList<E>
          implements List<E>, RandomAccess, Cloneable, java.io.Serializable
      

      CopyOnWriteArrayList 是故障安全的,并且在迭代期间修改底层 CopyOnWriteArrayList 时不会引发 ConcurrentModificationException,请使用 ArrayList 的单独副本。

      这通常成本太高,因为复制数组涉及每个更新操作,都会创建一个克隆副本。 CopyOnWriteArrayList 是只有频繁读操作的最佳选择。

      /**
               * Returns a shallow copy of this list.  (The elements themselves
               * are not copied.)
               *
               * @return a clone of this list
               */
              public Object clone() {
                  try {
                      @SuppressWarnings("unchecked")
                      CopyOnWriteArrayList<E> clone =
                          (CopyOnWriteArrayList<E>) super.clone();
                      clone.resetLock();
                      return clone;
                  } catch (CloneNotSupportedException e) {
                      // this shouldn't happen, since we are Cloneable
                      throw new InternalError();
                  }
              }
      

      【讨论】:

        【解决方案8】:

        因为获取位置和从给定位置获取元素的行为自然需要一些锁定(您不能让列表在这两个操作之间进行结构更改)。

        并发集合的理念是每个操作本身都是原子的,可以在没有显式锁定/同步的情况下完成。

        因此,从给定的List 获取位置为n 的元素作为原子操作在预期并发访问的情况下没有太大意义。

        【讨论】:

        • 约阿希姆,我认为你一针见血。举个例子,一个只读列表作为并发列表。从列表中获取位置 N 处的元素不仅有意义,而且是问题的简述。因此,不可变列表(小写 L)是一个很好的例子,但它不是列表(大写 L)。 CopyOnWriteArrayList 是并发的,但很多人不喜欢这种性能。沿着绳索(绳索)的解决方案可能会是一个很好的赢家。
        • 非常好。但是 OP 将使用的 List 可能有非常具体的用途。例如。它可能会在并发环境中填充,然后“锁定”(无论它是什么意思),然后通过索引安全访问。所以,在填充这样一个 List 的第一阶段仍然需要一个线程安全的实现。不幸的是,OP 没有具体说明他正在寻找的 List 将如何使用。
        猜你喜欢
        • 2012-05-13
        • 2016-02-03
        • 2018-10-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-27
        相关资源
        最近更新 更多