以线程安全的方式获取集合的内容

时间:2012-12-19 12:41:30

标签: java multithreading collections

我想以多线程方式读取 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也可以,但同样可能是偶然的。

如果你有一般的集合(不一定是列表)并且需要以多线程方式提取其内容,你在实践中如何做?

4 个答案:

答案 0 :(得分:2)

这取决于收藏品。如果在读取过程中没有发生结构变化 - 您可以同时读取它,这很好。大多数集合不会仅仅更改读取或迭代的结构,所以没关系,但在执行此操作之前,请务必阅读您正在使用的集合的文档。

例如,HashSet javadocs

  

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

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


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

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

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

答案 1 :(得分:2)

通常,迭代收集内容的成本不足以进行多线程处理。这是您在获取内容后对列表执行的操作。 所以你应该做的是:

  1. 使用单线程获取内容并划分工作量。
  2. 启动多个线程/作业来进行处理,为他们提供(大)工作量。确保线程不使用原始列表。
  3. 使用单个线程组合结果。
  4. 如果您需要共享集合,请使用线程安全集合。可以使用Collections。synchronized ......函数创建它们。但是请记住,这意味着线程必须等待彼此,如果你没有相当大的工作,这将使你的程序比单线程版本慢。

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

答案 2 :(得分:2)

您的用例将受益于使用队列 - 有一些线程安全的实现,例如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);
            }
        }
    };
}

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

答案 3 :(得分:1)

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

我更喜欢其中任何一个自己动手。

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

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