【问题标题】:Getting content of a collection in a thread-safe manner以线程安全的方式获取集合的内容
【发布时间】:2012-12-19 12:41:29
【问题描述】:

我想以多线程方式读取 java Collection 的内容。这里有很多关于相同上下文的问题,但没有关于特定阅读点的问题。

我有一个整数集合。我只想要几个线程来迭代它,每个线程一次拉一个整数。我想确保所有集合都被迭代,并且没有整数被两个不同的线程拉两次。

坦率地说,我不知道什么有效。我知道迭代器不是线程安全的,但是当谈到只读时我不知道。我做了一些测试来尝试获取线程错误,但没有达到 100% 的确定性:

int imax = 500;
Collection<Integer> li = new ArrayList<Integer>(imax);
for (int i = 0; i < imax; i++) {
    li.add(i);
}
final Iterator<Integer> it = li.iterator();

Thread[] threads = new Thread[20];
for (int i = 0; i < threads.length; i++) {
    threads[i] = new Thread("Thread " + i) {
        @Override
        public void run() {
            while(it.hasNext()) {
                System.out.println(it.next());
            }
        }
    };
}

for (int ithread = 0; ithread < threads.length; ++ithread) {
threads[ithread].setPriority(Thread.NORM_PRIORITY);
    threads[ithread].start();
}
try {
    for (int ithread = 0; ithread < threads.length; ++ithread)
    threads[ithread].join();
} catch (InterruptedException ie) {
    throw new RuntimeException(ie);
}

编辑: 在实际用例中,这个整数中的每一个都用于开始一项密集的工作,例如确定它是否是素数。

上例拉取整数列表,不重复不漏,不知道是不是机缘巧合。

使用 HashSet 代替 ArrayList 也可以,但同样,这可能是偶然的。

如果您有一个通用集合(不一定是列表)并且需要以多线程方式提取其内容,您在实践中会怎么做?

【问题讨论】:

    标签: java multithreading collections


    【解决方案1】:

    您的用例将受益于使用队列 - 有一些线程安全的实现,例如 ArrayBlockingQueue。

    Collection<Integer> li = new ArrayList<Integer>(imax);
    final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(li.size(), false, li);
    
    Thread[] threads = new Thread[20];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Thread("Thread " + i) {
            @Override
            public void run() {
                Integer i;
                while ((i = queue.poll()) != null) {
                    System.out.println(i);
                }
            }
        };
    }
    

    这是线程安全的,每个线程可以在初始集合的一部分上独立于其他线程工作。

    【讨论】:

    • 谢谢!我什至不知道队列。改变了我的生活!
    【解决方案2】:

    一般来说,通过迭代收集内容的成本不足以进行多线程。就是获取内容后对列表进行的操作。 所以你应该做的是:

    1. 使用单线程获取内容并划分工作负载。
    2. 启动几个线程/作业来进行处理,给它们一个(大)工作负载。确保线程不使用原始列表。
    3. 使用单个线程来组合结果。

    如果您需要共享集合,请使用线程安全集合。它们可以通过使用 Collections.synchronized... 函数来创建。但是请记住,这意味着线程必须相互等待,如果您没有大量工作,这将使您的程序比单线程版本慢。

    请注意,您在线程之间共享的所有对象都需要是线程安全的(例如,通过将所有访问包装在同步块中)。最好的信息来源是Concurrency in Practise

    【讨论】:

      【解决方案3】:

      这取决于收藏。如果在读取过程中没有发生结构变化 - 您可以同时读取它,这很好。大多数集合不会仅更改读取或迭代的结构,因此可以,但请确保在这样做之前阅读您正在使用的集合的文档。

      例如HashSet javadocs:

      请注意,此实现不同步。如果多个线程 同时访问一个哈希集,以及至少一个线程 修改集合,必须对外同步。

      这意味着同时从两个线程读取就可以了,只要没有写入。


      一种方法是拆分数据,让每个线程读取collection.size()/ numberOfThreads 元素。
      线程#i 将从collection.size()/numThreads * i 读取到collection.size()/numThreads * (i+1)

      (注意需要特别注意保证最后的元素不会丢失,可以通过将最后一个线程 frpm collection.size()/numThreads * i 设置为collection.size() 来完成,但它可能会使最后一个线程做更多的工作,并且会让你等待挣扎的线程)。

      另一种选择是使用间隔的任务队列,每个线程将在队列不为空时读取元素,并在给定的间隔内读取元素。队列必须同步,因为它被多个线程同时修改。

      【讨论】:

      • 好的,谢谢。因此,如果我回顾一下您指出的内容,这意味着我的不安全示例没有普遍的真理,最终将取决于 Collection 的具体实现。
      • @Jean-Yves:最终,是的。我不知道任何集合在阅读时实际上会改变结构 - 但这并不意味着没有,它最终取决于手头的具体实例。
      【解决方案4】:

      您可以使用java.util.Collections 提供的同步版本。或者您可以尝试java.util.concurrent 中的特殊数据结构(例如ConcurrentHashMap)。

      我更喜欢其中任何一个而不是自己滚动。

      另一个想法是在必要时同步整个方法,而不仅仅是集合访问。

      请记住,不可变对象始终是线程安全的。您只需要同步共享的可变状态。

      【讨论】:

      • 好的。但是你想:如果我同步整个方法,我失去了多线程的好处。
      • @Jean-Yves:不是这样。我相信ConcurrentHashMap 使用细粒度同步和/或readers-writer lock 允许同时进行多次读取。虽然可能是错的。但是,这种同步仍然有很多开销,如果您知道特定实例没有更改集合的状态,则可以避免这种开销。
      猜你喜欢
      • 2014-10-16
      • 1970-01-01
      • 2011-08-05
      • 2011-08-30
      • 1970-01-01
      • 2013-12-10
      • 1970-01-01
      • 1970-01-01
      • 2011-02-28
      相关资源
      最近更新 更多