Java发生在两个ConcurrentMaps上的一致线程视图之前

时间:2015-11-05 21:56:07

标签: java multithreading concurrency

我有一个java类来处理多线程订阅服务。通过实现Subscribable接口,可以将任务提交给服务并定期执行。代码草图如下所示:

import java.util.concurrent.*;

public class Subscribtions {

    private ConcurrentMap<Subscribable, Future<?>> futures = new ConcurrentHashMap<Subscribable, Future<?>>();
    private ConcurrentMap<Subscribable, Integer> cacheFutures = new ConcurrentHashMap<Subscribable, Integer>();
    private ScheduledExecutorService threads;

    public Subscribtions() {
        threads = Executors.newScheduledThreadPool(16);
    }

    public void subscribe(Subscribable subscription) {
        Runnable runnable = getThread(subscription);
        Future<?> future = threads.scheduleAtFixedRate(runnable, subscription.getInitialDelay(), subscription.getPeriod(), TimeUnit.SECONDS);
        futures.put(subscription, future);
    }

    /*
     * Only called from controller thread
     */
    public void unsubscribe(Subscribable subscription) {
        Future<?> future = futures.remove(subscription);    //1. Might be removed by worker thread 
        if (future != null)
            future.cancel(false);
        else {
            //3. Worker-thread view     := cacheFutures.put() -> futures.remove()
            //4. Controller-thread has seen futures.remove(), but has it seen cacheFutures.put()?
        }
    }

    /*
     * Only called from worker threads
     */
    private void delay(Runnable runnable, Subscribable subscription, long delay) {
        cacheFutures.put(subscription, 0);                  //2. Which is why it is cached first
        Future<?> currentFuture = futures.remove(subscription);
        if (currentFuture != null) {
            currentFuture.cancel(false);
            Future<?> future = threads.scheduleAtFixedRate(runnable, delay, subscription.getPeriod(), TimeUnit.SECONDS);
            futures.put(subscription, future);
        }
    }

    private Runnable getThread(Subscribable subscription) {
        return new Runnable() {
            public void run() {
                //Do work...
                boolean someCondition = true;
                long someDelay = 100;
                if (someCondition) {
                    delay(this, subscription, someDelay);
                }
            }
        };
    }

    public interface Subscribable {
        long getInitialDelay();
        long getPeriod();
    }
}

所以班级允许:

  • 订阅新任务
  • 取消订阅现有任务
  • 延迟定期执行的任务

外部控制线程添加/删除订阅,但延迟仅由内部工作线程引起。如果例如工作者线程没有发现上次执行的更新或者例如更新,则可能发生这种情况。如果线程只需要在00.00 - 23.00之间执行。

我的问题是工作线程可以调用delay()并从ConcurrentMap中删除它的未来,并且控制器线程可以同时调用unsubscribe()。然后,如果控制器线程在工作线程进入新的未来之前检查ConcurrentMap,则unsubscribe()调用将丢失。

有一些(可能不是详尽的清单)解决方案:

  • delay()unsubscribe()方法
  • 之间使用锁定
  • 与上述相同,但每次订阅一次锁定
  • (首选?)不使用锁定,但在delay()方法
  • 中“缓存”删除了期货

至于第三种解决方案,由于worker-thread已经建立了before-before关系cacheFutures.put() -> futures.remove(),并且ConcurrentMap的原子性使得控制器线程看到futures.remove(),它是否也看到同样的事情发生 - 作为工人线程的关系?即cacheFutures.put() -> futures.remove()?或者原子性是否仅适用于futures地图,其他变量的更新将在以后传播?

也欢迎任何其他意见,尤其是。考虑使用volatile关键字。缓存映射是否应声明为volatile?谢谢!

2 个答案:

答案 0 :(得分:0)

每个订阅一次锁定需要您维护另一个映射,并可能因此引入其他并发问题。我认为最好避免这种情况。这同样适用于缓存已删除的订阅,此外还会增加不必要的资源保留风险(请注意,它不是您需要缓存的Future本身,而是{与之相关的{1}}。

任何方式,你都需要某种同步/锁定。例如,在您的选项(3)中,您需要避免在Subscribable缓存该订阅和删除其unsubscribe()之间发生给定订阅的delay()。如果你没有某种形式的锁定就可以避免这种情况的唯一方法就是每个订阅只能使用一个Future,从Future注册之前就一直保持到位,直到subscribe()删除它为止。 {1}}。这样做与延迟已安排的订阅的能力不一致。

  

至于第三种解决方案,因为工作线程已经建立了之前发生的关系cacheFutures.put() - &gt; futures.remove(),并且ConcurrentMap的原子性使控制器线程看到期货.remove(),它是否也看到与工作线程相同的发生前关系?

发生之前是程序执行中的动作之间的关系。它并不特定于任何一个线程的执行视图。

  

或者原子性是否仅适用于期货地图,其他变量的更新将在以后传播?

控制器线程将始终看到unsubscribe()由同一调用执行的cacheFutures.put()之前发生的delay()调用执行。不过,我认为这对你没有帮助。

  

缓存映射是否应声明为volatile?

没有。这没有任何用处,因为虽然该地图的内容发生了变化,但地图本身始终是同一个对象,对它的引用不会改变。

您可以考虑让futures.remove()subscribe()delay()在呈现的unsubscribe()上进行同步。这不是我所理解的关于每次订阅锁定的意思,但它是类似的。它将避免需要单独的数据结构来维护这种锁。我想如果你想避免显式同步,你也可以在Subscribable接口中构建锁定方法。

答案 1 :(得分:0)

您有一个ConcurrentMap,但您没有使用它。请考虑以下几点:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

final class SO33555545
{

  public static void main(String... argv)
    throws InterruptedException
  {
    ScheduledExecutorService workers = Executors.newScheduledThreadPool(16);
    Subscriptions sub = new Subscriptions(workers);
    sub.subscribe(() -> System.out.println("Message received: A"));
    sub.subscribe(() -> System.out.println("Message received: B"));
    Thread.sleep(TimeUnit.SECONDS.toMillis(30));
    workers.shutdown();
  }

}

final class Subscriptions
{

  private final ConcurrentMap<Subscribable, Task> tasks = new ConcurrentHashMap<>();

  private final ScheduledExecutorService workers;

  public Subscriptions(ScheduledExecutorService workers)
  {
    this.workers = workers;
  }

  void subscribe(Subscribable sub)
  {
    Task task = new Task(sub);
    Task current = tasks.putIfAbsent(sub, task);
    if (current != null)
      throw new IllegalStateException("Already subscribed");
    task.activate();
  }

  private Future<?> schedule(Subscribable sub)
  {
    Runnable task = () -> {
      sub.invoke();
      if (Math.random() < 0.25) {
        System.out.println("Delaying...");
        delay(sub, 5);
      }
    };
    return workers.scheduleAtFixedRate(task, sub.getPeriod(), sub.getPeriod(), TimeUnit.SECONDS);
  }

  void unsubscribe(Subscribable sub)
  {
    Task task = tasks.remove(sub);
    if (task != null)
      task.cancel();
  }

  private void delay(Subscribable sub, long delay)
  {
    Task task = new Task(sub);
    Task obsolete = tasks.replace(sub, task);
    if (obsolete != null) {
      obsolete.cancel();
      task.activate();
    }
  }

  private final class Task
  {

    private final FutureTask<Future<?>> future;

    Task(Subscribable sub)
    {
      this.future = new FutureTask<>(() -> schedule(sub));
    }

    void activate()
    {
      future.run();
    }

    void cancel()
    {
      boolean interrupted = false;
      while (true) {
        try {
          future.get().cancel(false);
          break;
        }
        catch (ExecutionException ignore) {
          ignore.printStackTrace(); /* Cancellation is unnecessary. */
          break;
        }
        catch (InterruptedException ex) {
          interrupted = true; /* Keep waiting... */
        }
      }
      if (interrupted)
        Thread.currentThread().interrupt(); /* Reset interrupt state. */
    }

  }

}

@FunctionalInterface
interface Subscribable
{

  default long getPeriod()
  {
    return 4;
  }

  void invoke();

}