1

鉴于:

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

2 回答 2

2

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

不需要在此处执行双重检查锁定 (DCL)。(实际上,很少需要使用 DCL。在 99.9% 的情况下,常规锁定就可以了。现代 JVM 上的常规锁定足够快,以至于 DCL 的性能优势通常太小而无法引起明显区别。)

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

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

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

但是 Future 的 isDone() 方法呢?

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

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

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

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

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

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

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

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

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

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

于 2014-02-14T12:12:17.083 回答
1
  1. FutureTask中使用的AbstractQueuedSync具有线程的可变状态及其易失(线程安全)变量。所以不必担心 isDone() 方法。

    私有易失性 int 状态;

  2. 锁定对象的选择基于实例类型和情况,假设您有多个对象,并且它们在 TaskLauncher.class 上有 Sync 块,那么所有实例中的所有方法都通过这个单一的锁同步(如果你想使用这个在所有实例之间共享一个共享内存)。

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

于 2014-02-14T09:47:03.407 回答