ConcurrentHashMap of Future и блокировка с двойной проверкой

Данный:

  • Ленивый инициализированный одноэлементный класс, реализованный с шаблоном блокировки двойной проверки со всеми соответствующими 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 является потокобезопасным для операций чтения, поэтому мой get(key) является потокобезопасным и может не нуждаться в блокировке с двойной проверкой (но пока совершенно не уверен в этом…). А как насчет метода isDone() Future?
  2. Как выбрать правильный объект блокировки в блоке synchronized? Я знаю, что это не должно быть null, поэтому я сначала использую объект TaskLauncher.class в getInstance(), а затем уже инициализированный tasksCache в методе getTask(String key). И имеет ли этот выбор какое-то значение на самом деле?

person Doc Davluz    schedule 14.02.2014    source источник
comment
Я вижу одно потенциальное состояние гонки в getTask. Вы можете запустить две или более одинаковых задач, потому что вы не проверяете, что находится в кеше внутри вашего синхронизированного блока. Вам нужно извлечь свежее кешированное значение внутри вашего синхронизированного блока, иначе двойная проверка не сработает.   -  person Pavel Horal    schedule 14.02.2014
comment
Я говорю о том, что потокобезопасность ConcurrentHashMap не имеет отношения к тому, нужно ли вам использовать DCL. Это то, о чем вы спрашивали в своем Q1.   -  person Stephen C    schedule 14.02.2014


Ответы (2)


Нужно ли выполнять контроль блокировки с двойной проверкой в ​​моем методе getTask?

Здесь вам не нужно выполнять блокировку с двойной проверкой (DCL). (На самом деле, очень редко вам нужно использовать DCL. В 99,9% случаев обычная блокировка вполне подойдет. Обычная блокировка на современной JVM выполняется достаточно быстро, поэтому преимущество DCL в производительности составляет обычно слишком мало, чтобы иметь заметную разницу.)

Однако синхронизация необходима, если только вы не объявили tasksCache final. А если tasksCache не final, то простая блокировка вполне подойдет.

Я знаю, что ConcurrentHashMap является потокобезопасным для операций чтения...

Это не проблема. Проблема заключается в том, даст ли чтение значения ссылки taskCache правильное значение, если TaskLauncher создается и используется в разных потоках. Потокобезопасность выборки ссылки из переменной так или иначе не зависит от потокобезопасности объекта, на который делается ссылка.

А как насчет метода isDone() класса Future?

Опять же... это не имеет никакого отношения к тому, нужно ли вам использовать DCL или другую синхронизацию.

Для записи семантика памяти "контракт" для Future указана в javadoc:

"Эффекты согласованности памяти: действия, предпринимаемые асинхронными вычислениями, выполняются до действий, следующих за соответствующим Future.get() в другом потоке."

Другими словами, дополнительная синхронизация не требуется, когда вы вызываете get() на (правильно реализованном) Future.

Как выбрать правильный объект блокировки в синхронизированном блоке?

Блокировка служит для синхронизации доступа к переменным, читаемым и записываемым разными потоками, при удержании блокировки.

Теоретически вы можете написать все приложение, используя только одну блокировку. Но если бы вы это сделали, вы бы получили ситуацию, когда один поток ожидает другого, несмотря на то, что первому потоку не нужно использовать переменные, которые использовались другим. Таким образом, обычной практикой является использование блокировки, связанной с переменными.

Еще одна вещь, которую вам нужно убедиться, это то, что когда двум потокам требуется доступ к одному и тому же набору переменных, они используют один и тот же объект (или объекты) в качестве блокировок. Если они используют разные блокировки, то они не достигают должной синхронизации...

(Есть также вопросы о блокировке на this или на частной блокировке, а также о порядке, в котором блокировки должны быть получены. Но они выходят за рамки заданного вами вопроса.)

Это общие "правила". Чтобы принять решение в конкретном случае, вам нужно точно понимать, что вы пытаетесь защитить, и соответственно выбирать замок.

person Stephen C    schedule 14.02.2014
comment
Спасибо за ваш ответ. Я понимаю необходимость final для tasksCache, и на самом деле это так в моем коде (ссылка не может измениться). Но я не понимаю, почему вы говорите, что это не проблема. Наконец, есть ли у вас какой-либо указатель на документацию по тем вопросам, которые выходят за рамки? - person Doc Davluz; 14.02.2014
comment
Смотрите мой обновленный Вопрос. Лучшим справочником по всем аспектам параллелизма в Java является Java Concurrency in Practice от Goetz et al. Если у вас нет копии, купите ее. - person Stephen C; 14.02.2014
comment
Хорошо, спасибо за это хорошее объяснение. Ответ принят. Много раз слышал о Java Concurrency in Practice. Возможно, мне пора инвестировать. - person Doc Davluz; 14.02.2014

  1. #P1# <блочная цитата> #P2#
  2. Выбор объекта блокировки зависит от типа экземпляра и ситуации. Допустим, у вас есть несколько объектов, и у них есть блоки синхронизации в TaskLauncher.class, тогда все методы во всех экземплярах будут синхронизированы этой одиночной блокировкой (используйте это, если вы хотите совместно использовать одну общую память для всех экземпляров).

Если все экземпляры имеют собственную общую память, черно-белые потоки и методы используют это. Использование этого также сэкономит вам один дополнительный объект блокировки. В вашем случае вы можете использовать TaskLauncher.class, tasksCache, это все то же самое с точки зрения синхронизации, что и его синглтон.

person Rohit Sachan    schedule 14.02.2014
comment
Итак, моя блокировка с двойной проверкой совершенно бесполезна (пока я не сделаю другие разумные операции потокобезопасности в задаче создания)? - person Doc Davluz; 14.02.2014
comment
Я принял ответ @Stephen C, который является более полным. Еще раз спасибо. - person Doc Davluz; 14.02.2014