【问题标题】:ConcurrentHashMap of Future and double-check lockingFuture 的 ConcurrentHashMap 和双重检查锁定
【发布时间】:2014-02-14 09:02:17
【问题描述】:

给定:

  • 一个惰性初始化的单例类,使用双重检查锁定模式实现,所有相关的volatilesynchronized 都在getInstance 中。这个单例通过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) 是线程安全的,可能不需要双重检查锁定(但对此非常不确定……)。但是Future 的isDone() 方法呢?
  2. 如何在synchronized 块中选择正确的锁定对象?我知道它一定不是null,所以我首先在getInstance() 中使用TaskLauncher.class 对象,然后在getTask(String key) 方法中使用已经初始化的tasksCache。事实上,这个选择有什么重要性吗?

【问题讨论】:

  • 我在getTask 中看到了一种潜在的竞争条件。您可以启动两个或多个相同的任务,因为您没有检查同步块内缓存中的内容。您需要在同步块中提取新的缓存值,否则双重检查将不起作用。
  • 我的意思是ConcurrentHashMap的线程安全与你是否需要使用DCL无关。这就是您在 Q1 中提出的问题。

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


【解决方案1】:

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

您不需要在此处执行双重检查锁定 (DCL)。 (事实上​​,非常罕见需要使用 DCL。在 99.9% 的情况下,常规锁定就可以了。现代 JVM 上的常规锁定足够快,因此 DCL 的性能优势是通常太小而无法产生明显的差异。)

但是,除非您将 tasksCache 声明为 final,否则同步是必要的。如果tasksCache 不是final,那么简单的锁定就可以了。

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

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

但是 Future 的 isDone() 方法呢?

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

作为记录,Future 的内存语义“合同”在 javadoc 中指定:

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

换句话说,当您在(正确实现的)Future 上调用 get() 时,不需要额外的同步。

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

锁用于在持有锁的同时同步访问不同线程读写的变量。

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

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

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

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

【讨论】:

  • 感谢您的回复。我了解对 tasksCache 的最终需求,实际上在我的代码中就是这种情况(参考不能改变)。但我不明白你为什么说“那不是问题”。最后,您是否有任何关于超出范围的文档的指针?
  • 查看我更新的问题。与 Java 并发有关的所有事情的最佳参考是 Goetz 等人的“Java Concurrency in Practice”。如果您没有副本,请购买一份。
  • 好的,感谢您的精彩解释。回答接受。多次听说过“Java 并发实践”。也许是我投资的时候了。
【解决方案2】:
  1. AbstractQueuedSync 用在边 FutureTask 有一个变量 state thread 及其一个 volatile(线程安全)变量。所以不用担心 isDone() 方法。

    private volatile int 状态;

  2. 锁对象的选择基于实例类型和情况, 假设您有多个对象并且它们有同步块 TaskLauncher.class 然后所有实例中的所有方法都带有 通过这个单一的锁同步(如果你想共享一个 跨所有实例的单个共享内存)。

如果所有实例都有自己的共享内存 b/w 线程和方法,请使用 this。使用它也会为您节省一个额外的锁定对象。 在您的情况下,您可以使用 TaskLauncher.class ,tasksCache,这在同步方面都和它的单例一样。

【讨论】:

  • 那么,我的双重检查锁定完全没用(直到我不在创建任务中进行其他明智的线程安全操作)?
  • 我已经接受了更完整的@Stephen C 答案。再次感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多