【问题标题】:Synchronise concurrent requests to share results of a slow operation同步并发请求以共享慢操作的结果
【发布时间】:2019-04-17 09:24:32
【问题描述】:

我有一个 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 处理为尽快。

【问题讨论】:

    标签: java concurrency countdownlatch


    【解决方案1】:

    作为一种解决方案,您可以使用以下方法:

    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 final String result;
            private final LocalDateTime calculated;
    
            public ResultHolder(String result, LocalDateTime calculated) {
                this.result = result;
                this.calculated = calculated;
            }
        }
    }
    

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

    【讨论】:

      【解决方案2】:

      另一种方法(我认为可能更好)是将所有请求结果的线程添加到同步集合中。然后当结果到达时 - 将响应发送回线程。您可以使用 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 秒后得到结果。一种反应式编程;)它确实将方式改变为更异步的方式,但如果线程的调用者想要在获得结果的同时阻止执行,他可以实现它。该服务基本上是对所做的事情的自我解释。这就像说“我的操作很慢,所以我不会为你们每个调用者运行调用,而是在我准备好后将结果发送给你 - 给我一个消费者方法”

      【讨论】:

      • 不能保证两个线程不会同时读取isProcessing = false
      • @AleksandrSemyannikov 同意,​​看起来像一个错误。也不热衷于睡眠循环。
      • 是的,访问应该通过同步的 getter 我可以同意。至于睡眠循环,它可能不是最好的选择;)我只是想展示一下这个想法
      • 我添加了另一个我认为可能更好的解决方案 - 使用允许线程“订阅”结果而不是阻塞和等待的消费者接口
      【解决方案3】:

      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();
          }
        }
      }
      

      这里,读锁防止读过时状态,写锁防止多个线程同时执行“慢操作”。

      【讨论】:

      • 您检查结果 == null,但据我所知,它从未设置为 null(最初除外)。
      • 可能最好放 lock.writeLock().lock();在 if (result == null || staleResult()) 里面,你应该保证 lock.writeLock 已经解锁。我们还需要知道结果没有过期
      • @jon-hanson 只需使用一个队列放置 N 个结果副本(用于线程数),当所有项目被消耗时它会清除
      • @AleksandrSemyannikov 写锁仅在需要执行“慢操作”时才获得,并在其后立即释放。
      • @AlexSalauyou 你确定在 lock.writeLock().lock() 执行并且结果 == null || 是不可能的staleResult() 返回 false?
      【解决方案4】:

      简单的想法

      版本 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();
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2020-01-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-16
        • 1970-01-01
        • 2015-02-14
        • 1970-01-01
        相关资源
        最近更新 更多