并发检查集合是否为空

时间:2015-05-02 19:41:23

标签: java multithreading

我有这段代码:

private ConcurrentLinkedQueue<Interval> intervals = new ConcurrentLinkedQueue();
@Override
public void run(){
  while(!intervals.isEmpty()){
    //remove one interval
    //do calculations
    //add some intervals
  }
}

此代码由特定数量的线程同时执行。如您所见,循环应该继续,直到集合中没有剩余的间隔,但是存在问题。在每次迭代开始时,会从集合中删除一个间隔,最后可能会将一些间隔添加回同一集合中。

问题是,当一个线程在循环内部时,集合可能变为空,因此尝试进入循环的其他线程将无法执行此操作并过早地完成其工作,即使集合填充值。我希望线程计数保持不变(或不超过某个数字n),直到所有工作都完成。

这意味着当前没有线程在循环中工作,并且集合中没有剩余元素。有哪些可能的方法来实现这一目标?任何想法都受到欢迎。

在我的特定情况下解决此问题的一种方法是为每个线程提供原始集合的不同部分。但是在一个线程完成它的工作之后它将不再被程序使用,即使它可以帮助其他线程进行计算,所以我不喜欢这个解决方案,因为利用机器的所有内核很重要我的问题。

这是我能想到的最简单的最小工作示例。这可能是冗长的。

public class Test{   
   private ConcurrentLinkedQueue<Interval> intervals = new ConcurrentLinkedQueue();
   private int threadNumber;
   private Thread[] threads;
   private double result;

   public Test(int threadNumber){
      intervals.add(new Interval(0, 1));
      this.threadNumber = threadNumber;
      threads = new Thread[threadNumber];
   }

   public double find(){
      for(int i = 0; i < threadNumber; i++){
         threads[i] = new Thread(new Finder());
         threads[i].start();
      }
      try{
         for(int i = 0; i < threadNumber; i++){
            threads[i].join();
         }
      }
      catch(InterruptedException e){
         System.err.println(e);
      }
      return result;
   }   

   private class Finder implements Runnable{   
      @Override
      public void run(){
         while(!intervals.isEmpty()){
            Interval interval = intervals.poll();
            if(interval.high - interval.low > 1e-6){    
               double middle = (interval.high + interval.low) / 2;
               boolean something = true;
               if(something){
                  intervals.add(new Interval(interval.low + 0.1, middle - 0.1));
                  intervals.add(new Interval(middle + 0.1, interval.high - 0.1));
               }
               else{
                  intervals.add(new Interval(interval.low + 0.1, interval.high - 0.1));
               }
            }
         }
      }
   }

   private class Interval{
      double low;
      double high;
      public Interval(double low, double high){
         this.low = low;
         this.high = high;
      }
   }
}

您可能需要了解的程序:每次迭代间隔应该消失(因为它太小),变小或分成两个较小的间隔。没有间隔后,工作完成。此外,我应该能够限制使用某些数字n执行此工作的线程数。实际程序通过划分间隔并使用某些规则丢弃那些不能包含最大值的区间的部分来查找某个函数的最大值,但这不应该与我的问题真正相关。

5 个答案:

答案 0 :(得分:2)

您可以使用原子标记,即:

private ConcurrentLinkedQueue<Interval> intervals = new ConcurrentLinkedQueue<>();
private AtomicBoolean inUse = new AtomicBoolean();

@Override
public void run() {
    while (!intervals.isEmpty() && inUse.compareAndSet(false, true)) {
        // work
        inUse.set(false);
    }
}

UPD

问题已更新,所以我会给你更好的解决方案。它更经典&#34;使用阻塞队列的解决方案;

private BlockingQueue<Interval> intervals = new ArrayBlockingQueue<Object>();
private volatile boolean finished = false;

@Override
public void run() {
    try {
        while (!finished) {
            Interval next = intervals.take();
            // put work there
            // after you decide work is finished just set finished = true
            intervals.put(interval); // anyway, return interval to queue
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

UPD2

现在最好重新编写解决方案并将范围划分为每个线程的子范围。

答案 1 :(得分:1)

您的问题看似递归 - 处理一个任务(间隔)可能会产生一些子任务(子间隔)。

为此,我会使用RecursiveTaskclass Interval { ... } class IntervalAction extends RecursiveAction { private Interval interval; private IntervalAction(Interval interval) { this.interval = interval; } @Override protected void compute() { if (...) { // we need two sub-tasks IntervalAction sub1 = new IntervalAction(new Interval(...)); IntervalAction sub2 = new IntervalAction(new Interval(...)); sub1.fork(); sub2.fork(); sub1.join(); sub2.join(); } else if (...) { // we need just one sub-task IntervalAction sub3 = new IntervalAction(new Interval(...)); sub3.fork(); sub3.join(); } else { // current task doesn't need any sub-tasks, just return } } } public static void compute(Interval initial) { ForkJoinPool pool = new ForkJoinPool(); pool.invoke(new IntervalAction(initial)); // invoke will return when all the processing is completed }

{{1}}

答案 2 :(得分:1)

我遇到了同样的问题,我测试了以下解决方案。

在我的测试示例中,我有一个填充整数的队列(相当于你的间隔)。对于测试,在每次迭代时,如果新值低于7(任意),则从队列中取出一个数字,递增并放回队列中。这与机制上的间隔生成具有相同的影响。

这是一个示例工作代码(请注意,我在java 1.8中开发并使用Executor framework来处理我的线程池。):

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;

public class Test {
    final int numberOfThreads;
    final BlockingQueue<Integer> queue;
    final BlockingQueue<Integer> availableThreadsTokens;
    final BlockingQueue<Integer> sleepingThreadsTokens;
    final ThreadPoolExecutor executor;

    public static void main(String[] args) {
        final Test test = new Test(2); // arbitrary number of thread => 2
        test.launch();
    }

    private Test(int numberOfThreads){
        this.numberOfThreads = numberOfThreads;
        this.queue = new PriorityBlockingQueue<Integer>(); 
        this.availableThreadsTokens = new LinkedBlockingQueue<Integer>(numberOfThreads);
        this.sleepingThreadsTokens = new LinkedBlockingQueue<Integer>(numberOfThreads);
        this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(numberOfThreads);
    }

    public void launch() {

        // put some elements in queue at the beginning
        queue.add(1);
        queue.add(2);
        queue.add(3);

        for(int i = 0; i < numberOfThreads; i++){
            availableThreadsTokens.add(1);
        }

        System.out.println("Start");

        boolean algorithmIsFinished = false;

        while(!algorithmIsFinished){
            if(sleepingThreadsTokens.size() != numberOfThreads){
                try {
                    availableThreadsTokens.take();
                } catch (final InterruptedException e) {
                    e.printStackTrace();
                    // some treatment should be put there in case of failure
                    break;
                }
                if(!queue.isEmpty()){ // Continuation condition
                    sleepingThreadsTokens.drainTo(availableThreadsTokens);
                    executor.submit(new Loop(queue.poll(), queue, availableThreadsTokens));
                }
                else{
                    sleepingThreadsTokens.add(1);
                }
            }
            else{
                algorithmIsFinished = true;
            }
        }

        executor.shutdown();
        System.out.println("Finished");
    }

    public static class Loop implements Runnable{
        int element;
        final BlockingQueue<Integer> queue;
        final BlockingQueue<Integer> availableThreadsTokens;

        public Loop(Integer element, BlockingQueue<Integer> queue, BlockingQueue<Integer> availableThreadsTokens){
            this.element = element;
            this.queue = queue;
            this.availableThreadsTokens = availableThreadsTokens;
        }

        @Override
        public void run(){
            System.out.println("taking element "+element);
            for(Long l = (long) 0; l < 500000000L; l++){
            }
            for(Long l = (long) 0; l < 500000000L; l++){
            }
            for(Long l = (long) 0; l < 500000000L; l++){
            }
            if(element < 7){
                this.queue.add(element+1);
                System.out.println("Inserted element"+(element + 1));
            }
            else{
                System.out.println("no insertion");
            }
            this.availableThreadsTokens.offer(1);
        }
    }   
}

我运行此代码进行检查,它似乎正常工作。然而,肯定会有一些改进:

  • sleepingThreadsT​​okens不一定是BlockingQueue,因为只有主要访问它。我使用了这个界面,因为它允许一个很好的sleepingThreadsT​​okens.drainTo(availableThreadsT​​okens);
  • 我不确定队列是否必须阻塞,因为只有主队从中获取并且不等待元素(它只等待令牌)。
  • ...

这个想法是主线程检查终止,为此它必须知道当前有多少线程正在工作(因此它不会过早地停止算法,因为队列是空的)。为此,将创建两个特定队列:availableThreadsT​​okens和sleepingThreadsT​​okens。 availableThreadsT​​okens中的每个元素都表示已完成迭代的线程,并等待另一个线程。 sleepingThreadsT​​okens中的每个元素都表示可用于进行新迭代的线程,但队列为空,因此它没有工作并且进入“休眠”状态。所以在每个时刻availableThreadsT​​okens.size()+ sleepingThreadsT​​okens.size()= numberOfThreads - threadExcecutingIteration。

请注意,availableThreadsT​​okens和sleepingThreadsT​​okens上的元素仅表示线程活动,它们不是线程,也不是设计特定线程。

终止案例:假设我们有N个线程(aribtrary,固定号码)。 N个线程正在等待工作(在availableThreadsT​​okens中有N个令牌),队列中只剩下1个元素,并且此元素的处理不会生成任何其他元素。 Main获取第一个标记,发现队列不为空,轮询元素并发送线程工作。 N-1个下一个令牌逐个被消耗,并且由于队列为空,令牌被逐个移动到sleepingThreadsT​​okens中。 Main知道循环中有1个线程工作,因为availableThreadsT​​okens中没有令牌,sleepThreadsT​​okens中只有N-1,所以它等待(.take())。当线程完成并释放令牌Main消耗它时,发现队列现在为空并将最后一个令牌放在sleepingThreadsT​​okens中。由于所有令牌现在处于sleepingThreadsT​​okens Main知道1)所有线程都处于非活动状态2)队列为空(否则最后一个令牌不会被转移到sleepingThreadsT​​okens,因为线程会接受这个工作)。

请注意,如果工作线程在所有availableThreadsT​​okens移动到sleepingThreadsT​​okens之前完成处理,则没有任何区别。

现在,如果我们假设最后一个元素的处理会在队列中生成M个新元素,那么Main会将所有来自sleepingThreadsT​​okens的标记放回到availableThreadsT​​okens,并开始再次为它们分配处理。即使M&lt;&lt; N因为我们不知道将来会插入多少元素,所以我们必须保持所有线程可用。

答案 3 :(得分:0)

我会建议一个主人/工人的方法。

主进程遍历时间间隔并将该时间间隔的计算分配给不同的进程。它还根据需要删除/添加。这样,所有核心都被利用,并且只有当所有间隔都完成时,才完成该过程。这也称为动态工作分配。

一个可能的例子:

public void run(){
  while(!intervals.isEmpty()){
    //remove one interval
    Thread t = new Thread(new Runnable()
    {
        //do calculations
    });
    t.run();
    //add some intervals
  }
}

您提供的可能解决方案称为静态分配,并且您将更正,它将以最慢处理器的速度完成,但动态方法将利用所有内存。

答案 4 :(得分:0)

我也遇到过这个问题。我解决它的方法是使用AtomicInteger来了解队列中的内容。在每个offer()之前递增整数。每次poll()后递减整数。 CLQ没有真正的isEmpty(),因为它必须查看头/尾节点,这可以原子地改变(CAS)。

这并不保证100%某个线程可能会在另一个线程递减后递增,因此您需要在结束线程之前再次检查。它比依赖while(... isEmpty())

更好

除此之外,您可能需要同步。