同步并发请求以共享操作缓慢的结果

时间:2018-11-14 11:58:48

标签: java concurrency countdownlatch

我有一个Java UI服务,该服务具有一个API方法,该方法调用相对较慢的操作(例如,约30秒)。该操作是无参数的,但是它对确实随时间变化(相对缓慢)的外部数据进行操作。返回最新结果的方法并不重要-如果它们已经使用30秒,那么就可以接受。

最终,我需要优化慢速操作的实现,但是作为短期解决方案,我想使该操作互斥,以便在另一个传入请求(在单独的线程上)尝试调用时在另一个操作正在进行时进行操作,然后第二个操作阻塞,直到第一个操作完成。然后,第二个线程使用该操作的第一次调用的结果-即它不再尝试再次运行该操作。

例如:

class MyService {
    String serviceApiMmethod() {
       // If a second thread attempts to call this method while another is in progress
       // then block here until the first returns and then use those results
       // (allowing it to return immediately without a second call to callSlowOperation).
       return callSlowOperation();
    }
}

在Java(8)中,首选的通用方法是什么。我猜我可以使用CountDownLatch,但尚不清楚如何最好地在线程之间共享结果。是否存在现有的并发原语来简化此操作?

编辑:一旦所有线程都使用了该结果(即将其返回给调用方),我需要清除对结果的所有引用,因为它是一个相对较大的对象,需要对其进行GC处理尽快。

4 个答案:

答案 0 :(得分:1)

简单的想法

版本1:

class Foo {
    public String foo() throws Exception {
        synchronized (this) {
            if (counter.incrementAndGet() == 1) {
                future = CompletableFuture.supplyAsync(() -> {
                    try {
                        Thread.sleep(1000 * (ThreadLocalRandom.current().nextInt(3) + 1));
                    } catch (InterruptedException e) {
                    }
                    return "ok" + ThreadLocalRandom.current().nextInt();
                });
            }
        }

        String result = future.get();
        if (counter.decrementAndGet() == 0) {
            future = null;
        }

        return result;
    }

    private AtomicInteger counter = new AtomicInteger();
    private Future<String> future;
}

版本2:与@AleksandrSemyannikov一起

public class MyService {
    private AtomicInteger counter = new AtomicInteger();
    private volatile String result;

    public String serviceApiMethod() {
        counter.incrementAndGet();
        try {
            synchronized (this) {
                if (result == null) {
                    result = callSlowOperation();
                }
            }
            return result;
        } finally {
            if (counter.decrementAndGet() == 0) {
                synchronized (this) {
                    if (counter.get() == 0) {
                        result = null;
                    }
                }
            }
        }
    }

    private String callSlowOperation() {
        try {
            Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return Thread.currentThread().getName();
    }
}

答案 1 :(得分:0)

作为解决方案,您可以使用以下方式:

public class MyService {

    private volatile ResultHolder holder;

    public String serviceApiMethod() {
        if (holder != null && !isTimedOut(holder.calculated)) {
            return holder.result;
        }
        synchronized (this) {
            if (holder != null && !isTimedOut(holder.calculated)) {
                return holder.result;
            }
            String result = callSlowOperation();
            holder = new ResultHolder(result, LocalDateTime.now());
            return result;
        }
    }

    private static class ResultHolder {
        private String result;
        private LocalDateTime calculated;

        public ResultHolder(String result, LocalDateTime calculated) {
            this.result = result;
            this.calculated = calculated;
        }
    }
}

请注意,MyService必须是单例,而ResultHolder必须是不可变的

答案 2 :(得分:0)

另一种方法(我认为可能更好)是将所有要求结果的线程添加到同步集合中。然后,当结果到达时-将响应发送回线程。您可以使用Java 8功能接口使用者来使其更加精美。它不会浪费CPU时间(例如使用thread.sleep甚至使用countDownLatch和其他现代Java并发类)。它将要求这些线程具有一个回调方法来接受结果,但它甚至可能使您的代码更易于阅读:

class MyService {
    private  static volatile boolean isProcessing;
    private synchronized static  boolean  isProcessing() {
        return isProcessing;
    }
    private static Set<Consumer<String>> callers=Collections.synchronizedSet(new HashSet<>());

    void serviceApiMmethod(Consumer<String> callBack) {
       callers.add(callBack);
       callSlowOperation();
    }

    private synchronized static  void callSlowOperation() {
        if(isProcessing())
            return;
        isProcessing=true;
        try { Thread.sleep(1000); }catch (Exception e) {}//Simulate slow operation
        final String result="slow result";
        callers.forEach(consumer-> consumer.accept(result));
        callers.clear();
        isProcessing=false;

    }
}

调用线程:

class ConsumerThread implements Runnable{
    final int threadNumber;
    public ConsumerThread(int num) {
        this.threadNumber=num;

    }
    public void processResponse(String response) {
        System.out.println("Thread ["+threadNumber+"] got response:"+response+" at:"+System.currentTimeMillis());
    }

    @Override
    public void run() {
            new MyService().serviceApiMmethod(this::processResponse);
    }


}

这样,最终对象将被垃圾回收,因为所有消费者都将立即获取并释放它。

并测试结果:

public class Test{
    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis());
        for(int i=0;i<5;i++) {
            new Thread(new ConsumerThread(i)).start();
        }
    }
}

结果:

1542201686784
Thread [1] got response:slow result at:1542201687827
Thread [2] got response:slow result at:1542201687827
Thread [3] got response:slow result at:1542201687827
Thread [0] got response:slow result at:1542201687827
Thread [4] got response:slow result at:1542201687827

所有线程在1秒后获得结果。一种反应式编程;)它的确将方法转换为一种更加异步的方法,但是如果线程的调用者希望在获得结果的同时阻止执行,则可以实现。该服务基本上可以自我解释所完成的工作。这就像在说:“我的操作很慢,所以不用为每个呼叫者运行呼叫,而是可以在准备好后将结果发送给您-给我一种使用方方法”

答案 3 :(得分:0)

ReentrantReadWriteLock将更易于使用:

class MyService {

  String result;
  ReadWriteLock lock = new ReentrantReadWriteLock(); 

  String serviceApiMmethod() {
    lock.readLock().lock();
    try {
      if (result == null || staleResult()) {
        lock.readLock().unlock();
        lock.writeLock().lock();
        try {
          if (result == null || staleResult()) {
            result = callSlowOperation();
          }
        } finally {
          lock.writeLock().unlock();
          lock.readLock().lock();
        }
      }
      return result;
    } finally {
       lock.readLock().unlock();
    }
  }
}

在这里,读锁定可防止读取陈旧状态,写锁定可防止模拟多个威胁同时执行“慢速操作”。