Future的ConcurrentHashMap和双重检查锁定

时间:2014-02-14 09:02:17

标签: java synchronized future concurrenthashmap double-checked-locking

假设:

  • 使用双重检查锁定模式实现的惰性初始化单例类,其中包含volatile中所有相关的synchronizedgetInstance内容。该单例通过ExecutorService
  • 启动异步操作
  • 有七种类型的任务,每种都由一个唯一的密钥标识,
  • 启动任务时,它将存储在基于ConcurrentHashMap
  • 的缓存中
  • 当客户端请求任务时,如果缓存中的任务完成,则启动并缓存新任务;如果它正在运行,则从缓存中检索任务并将其传递给客户端。

以下是代码的摘录:

private static volatile TaskLauncher instance;
private ExecutorService threadPool;
private ConcurrentHashMap<String, Future<Object>> tasksCache;

private TaskLauncher() {
    threadPool = Executors.newFixedThreadPool(7);
    tasksCache = new ConcurrentHashMap<String, Future<Object>>();
}

public static TaskLauncher getInstance() {
    if (instance == null) {
        synchronized (TaskLauncher.class) {
            if (instance == null) {
                instance = TaskLauncher();
            }
        }
    }
    return instance;
}

public Future<Object> getTask(String key) {
    Future<Object> expectedTask = tasksCache.get(key);
    if (expectedTask == null || expectedTask.isDone()) {
        synchronized (tasksCache) {
            if (expectedTask == null || expectedTask.isDone()) {
                // Make some stuff to create a new task
                expectedTask = [...];
                threadPool.execute(expectedTask);
                taskCache.put(key, expectedTask);
            }
        }
    }
    return expectedTask;
}

我有一个主要问题,还有一个小问题:

  1. 我是否需要在getTask方法中执行双重检查锁定控制?我知道ConcurrentHashMap对于读操作是线程安全的,所以我的get(key)是线程安全的,可能不需要双重检查锁定(但是还不太确定这个......)。但是未来的isDone()方法呢?
  2. 如何在synchronized区块中选择正确的锁定对象?我知道它不能是null,因此我首先使用TaskLauncher.class中的getInstance()对象,然后使用tasksCache方法中已经初始化的getTask(String key)。事实上这个选择有什么重要性吗?

2 个答案:

答案 0 :(得分:2)

  

我是否需要在getTask方法中执行双重检查锁定控制?

您不需要需要进行双重检查锁定(DCL)。 (实际上,非常罕见你需要使用DCL。在99.9%的情况下,常规锁定就好了。现代JVM上的常规锁定足够快,以至于DCL的性能优势是通常太小而不能产生明显的差异。)

但是,除非您将tasksCache声明为final,否则同步。如果tasksCache不是final,那么简单锁定应该没问题。

  

我知道ConcurrentHashMap对于读取操作是线程安全的......

这不是问题。问题是,如果在不同的线程上创建并使用taskCache,那么读取TaskLauncher引用的值是否会为您提供正确的值。从变量中获取引用的线程安全性不会受到引用对象的线程安全性的影响。

  

但是Future的isDone()方法呢?

再次......这与您是否需要使用DCL或其他同步无关。

对于记录,Future的内存语义“契约”在javadoc中指定:

  

“内存一致性影响:异步计算所采取的操作发生在另一个线程中相应的Future.get()之后的操作之前。”

换句话说,当您(正确实施)get()上调用Future时,无需额外同步。

  

如何在同步块中选择正确的锁定对象?

锁定用于同步访问由不同线程读取和写入的变量,同时保持锁定。

理论上,您可以编写整个应用程序以仅使用一个锁。但是如果你这样做,你会得到一个线程等待另一个线程的情况,尽管第一个线程不需要使用另一个线程使用的变量。因此,通常的做法是使用与变量关联的锁。

您需要确保的另一件事是,当两个线程需要访问同一组变量时,它们使用与锁相同的对象(或对象)。如果他们使用不同的锁,那么他们就无法实现正确的同步......

(还有关于锁定this或私有锁的问题,以及关于应该获取锁定的顺序。但这些超出了您提出的问题的范围。)

这些是一般的“规则”。要在特定情况下做出决定,您需要准确了解您要保护的内容,并相应地选择锁定。

答案 1 :(得分:1)

    在方AbstractQueuedSync中使用的
  1. FutureTask具有变量状态 线程及其易失性(线程安全)变量。所以不必担心isDone()方法。

      

    private volatile int state;

  2. 锁定对象的选择基于实例类型和情况, 假设您有多个对象,并且它们具有同步块 TaskLauncher.class然后是所有实例中的所有方法 通过此单一锁定同步(如果要共享,请使用此锁定 所有实例中的单个共享内存)。
  3. 如果所有实例都有自己的共享内存,则使用这个。使用它还可以为您节省一个额外的锁定对象。 在你的情况下你可以使用 TaskLauncher.class,tasksCache,这在singelton的同步方面完全相同。