ThreadLocal HashMap против ConcurrentHashMap для потоковообезопасных несвязанных кешей

Я создаю кеш мемоизации со следующими характеристиками:

  • a cache miss will result in computing and storing an entry
    • this computation is very expensive
    • это вычисление идемпотентно
  • unbounded (entries never removed) since:
    • the inputs would result in at most 500 entries
    • каждая сохраненная запись очень мала
    • кеш относительно недолговечен (обычно менее часа)
    • в целом использование памяти не является проблемой
  • будут тысячи чтений - за время жизни кеша я ожидаю 99,9% + попаданий в кеш
  • должен быть потокобезопасным

Что будет иметь лучшие характеристики или при каких условиях одно решение будет предпочтительнее другого?

ThreadLocal HashMap:

class MyCache {
    private static class LocalMyCache {
        final Map<K,V> map = new HashMap<K,V>();

        V get(K key) {
            V val = map.get(key);
            if (val == null) {
                val = computeVal(key);
                map.put(key, val);
            }
            return val;
        }
    }

    private final ThreadLocal<LocalMyCache> localCaches = new ThreadLocal<LocalMyCache>() {
        protected LocalMyCache initialValue() {
            return new LocalMyCache();
        }
    };

    public V get(K key) {
        return localCaches.get().get(key);
    }
}

ConcurrentHashMap:

class MyCache {
    private final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<K,V>();

    public V get(K key) {
        V val = map.get(key);
        if (val == null) {
            val = computeVal(key);
            map.put(key, val);
        }
        return val;
    }
}

Я полагаю, что решение ThreadLocal изначально было бы медленнее, если бы было много потоков из-за всех пропусков кеша на поток, но за тысячи чтений амортизированная стоимость будет ниже, чем решение ConcurrentHashMap. Моя интуиция верна?

Или есть еще лучшее решение?


person Maian    schedule 12.01.2013    source источник


Ответы (6)


использовать ThreadLocal, поскольку кеш - не лучшая практика

В большинстве контейнеров потоки повторно используются через пулы потоков и, следовательно, никогда не являются gc. это приведет к чему-то проводному

используйте ConcurrentHashMap, вы должны управлять им, чтобы предотвратить утечку памяти

Если вы настаиваете, я предлагаю использовать недельную или мягкую ссылку и выселять после богатого максимального размера

если вы нашли решение для кеширования памяти (не изобретайте велосипед), попробуйте кеш guava http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/cache/CacheBuilder.html.

person farmer1992    schedule 12.01.2013
comment
Я не могу понять этого ответа. Пожалуйста, переформулируйте его на стандартном английском языке. Кажется, вы рекомендуете одновременно два разных и несовместимых ответа. - person user207421; 13.01.2013
comment
Как упоминалось в описании, кеш недолговечен, и ввод гарантирует ограниченное количество ключей. Таким образом, память в течение всего срока службы кеша не является проблемой. Кроме того, я почти уверен, что ThreadLocals можно очистить, как только объект MyCache будет иметь право на сборку мусора, поэтому утечки памяти быть не должно. - person Maian; 13.01.2013
comment
С учетом сказанного, я могу попробовать Google CacheBuilder. Похоже, что у него гораздо больше функций, чем мне нужно - для неограниченного кеша мне не нужны накладные расходы на политики истечения срока действия. - person Maian; 13.01.2013
comment
@Maian guava cacah предоставляет параметр concurrencyLevel, который вы можете настроить для повышения производительности. что-то вроде изменения количества разделов ConcurrentHashMap, чтобы избежать блокировки конфликта - person farmer1992; 13.01.2013
comment
@EJP отредактировал. Я просто рекомендую использовать хорошую структуру кеширования вместо того, чтобы создавать ее. ThreadLocal и ConcurrentHashMap не рекомендуются - person farmer1992; 13.01.2013
comment
Получается, что использование ThreadLocals для кешей - плохая идея, если вы не можете полностью контролировать потоки. Это особенно плохо, если вы создаете экземпляр ThreadLocal для каждого кеша. У ThreadLocal странный жизненный цикл - содержимое потока собирается сборщиком мусора, только если оно явно удалено или поток умирает. Для этого варианта использования мемоизации достаточно ConcurrentHashMap. CacheBuilder от Guava был бы лучше, но это не из-за политики выселения (в данном случае ненужной); это потому, что он поддерживает функцию вычислений на лету с помощью LoadingCache. - person Maian; 14.04.2013

это вычисление очень дорого

Я предполагаю, что это причина, по которой вы создали кеш, и это должно быть вашей основной проблемой.

Хотя скорость решений может немного отличаться ‹< 100 нс, я подозреваю, что более важно, чтобы вы могли делиться результатами между потоками. то есть ConcurrentHashMap, вероятно, будет лучшим для вашего приложения, так как он, вероятно, сэкономит вам больше процессорного времени в долгосрочной перспективе.

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

person Peter Lawrey    schedule 12.01.2013
comment
В конечном итоге это зависит от отношения совокупного времени вычислений к совокупному времени чтения. Думаю, я просто собираюсь протестировать каждое решение и профилировать их. - person Maian; 13.01.2013
comment
Если у вас высокое соотношение чтения и записи, вы получите почти такую ​​же производительность, как и локальные карты. Он также имеет 16 разделов, поэтому, если у вас есть несколько потоков, которые пытаются писать одновременно, они могут делать это без конкуренции, если они попадают в разные разделы. - person Peter Lawrey; 13.01.2013

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

Стоит отметить, что ConcurrentHashMap хорошо масштабируется и хорошо работает в условиях высокой конкуренции. Я не знаю, будет ли ThreadLocal работать лучше.

Помимо использования библиотеки, вы можете почерпнуть вдохновение из Java Concurrency in Practice Listing 5.19. Идея состоит в том, чтобы сохранить на карте Future<V> вместо V. Это очень помогает сделать весь метод потокобезопасным, оставаясь при этом эффективным (без блокировок). Я вставил приведенную ниже реализацию для справки, но эту главу стоит прочитать, чтобы понять, что важна каждая деталь.

public interface Computable<K, V> {

    V compute(K arg) throws InterruptedException;
}

public class Memoizer<K, V> implements Computable<K, V> {

    private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>();
    private final Computable<K, V> c;

    public Memoizer(Computable<K, V> c) {
        this.c = c;
    }

    public V compute(final K arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(eval);
                f = cache.putIfAbsent(arg, ft);
                if (f == null) {
                    f = ft;
                    ft.run();
                }
            }
            try {
                return f.get();
            } catch (CancellationException e) {
                cache.remove(arg, f);
            } catch (ExecutionException e) {
                throw new RuntimeException(e.getCause());
            }
        }
    }
}
person assylias    schedule 12.01.2013
comment
Он потокобезопасен в том смысле, что вычисления идемпотентны. Мое решение имеет только неоптимальную стоимость промахов кеша. Я мог бы использовать ваше решение, но, похоже, есть относительно большие накладные расходы на попадания в кеш, когда подавляющее большинство моих чтений (например, 99,9% +) будут попаданиями в кеш. - person Maian; 13.01.2013
comment
@Maian Согласовано: это зависит от вашего шаблона использования, но я не думаю, что у альтернативы так много накладных расходов на попадания в кеш (возможно, ни одного раза после JIT). - person assylias; 13.01.2013

Учитывая, что реализовать оба из них относительно легко, я бы посоветовал вам попробовать их оба и протестировать при постоянной нагрузке, чтобы увидеть, какой из них лучше всего подходит для вашего приложения.

Я предполагаю, что ConcurrentHashMap будет немного быстрее, поскольку ему не нужно делать нативные вызовы Thread.currentThread(), как это делает ThreadLocal. Однако это может зависеть от объектов, которые вы храните, и от того, насколько эффективно их хеш-кодирование.

Возможно, мне также стоит попытаться настроить concurrencyLevel параллельной карты на необходимое количество потоков. По умолчанию - 16.

person AngerClown    schedule 12.01.2013
comment
и Thread.currentThread(), и ThreadLocal очень быстрые. - person irreputable; 12.01.2013
comment
В самом деле, я думаю, мне просто нужно протестировать и профилировать каждое решение (включая предлагаемое полностью мемоизированное решение ConcurrentHashMap). - person Maian; 13.01.2013

Скорость поиска, вероятно, одинакова в обоих решениях. Если нет других проблем, я бы предпочел ThreadLocal, поскольку лучшее решение проблем с многопоточностью - это однопоточность.

Однако ваша основная проблема в том, что вам не нужны параллельные вычисления для одного и того же ключа; так что на ключ должен быть замок; такие блокировки обычно могут быть реализованы с помощью ConcurrentHashMap.

Итак, мое решение было бы

class LazyValue
{
    K key;

    volatile V value;

    V getValue() {  lazy calculation, doubled-checked locking }
}


static ConcurrentHashMap<K, LazyValue> centralMap = ...;
static
{
    for every key
        centralMap.put( key, new LazyValue(key) );
}


static V lookup(K key)
{
    V value = localMap.get(key);
    if(value==null)
        localMap.put(key, value=centralMap.get(key).getValue())
    return value;
}
person irreputable    schedule 12.01.2013
comment
Это решение в некоторой степени похоже на решение FutureTask от assylias в том, что оно использует летучие элементы (вместо блокировок), и оба предназначены для устранения избыточных вычислений. Ваше решение также немного странно, поскольку оно использует как ThreadLocal, так и общий ConcurrentHashMap - в этом случае мне интересно, будет ли лучше просто использовать ConcurrentHashMap (таким образом, чтобы избежать избыточных вычислений). Мне также не нравится использование статической ConcurrentHashMap - я хочу, чтобы кеш был недолговечным, и не хочу, чтобы память могла увеличиваться в течение нескольких дней (и я не хочу вводить политики истечения срока действия). - person Maian; 13.01.2013

Вопрос производительности не имеет значения, поскольку решения не эквивалентны.

Хеш-карта ThreadLocal не используется совместно между потоками, поэтому вопрос о безопасности потоков даже не возникает, но она также не соответствует вашей спецификации, в которой ничего не говорится о каждом потоке, имеющем собственный кеш.

Требование безопасности потоков подразумевает, что один кеш используется всеми потоками, что полностью исключает использование ThreadLocal.

person user207421    schedule 13.01.2013
comment
ThreadLocal отвечает за безопасность потоков MyCache.get (). Безопасность потоков не подразумевает использования общих объектов. См. en.wikipedia.org/wiki/Thread_safety. - person Maian; 13.01.2013