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

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

  • пропуск кэша приведет к вычислению и хранению записи
    • это вычисление очень дорого
    • это вычисление идемпотентно
  • unbounded (записи никогда не удаляются), так как:
    • входные данные приведут к не более 500 записей
    • каждая сохраненная запись очень мала
    • кэш относительно shorted-жил (как правило, менее часа)
    • в целом, использование памяти не проблема
  • там будут тысячи чтений-за время жизни кэша, я ожидаю 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. Верна ли моя интуиция?

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

6 ответов


использовать ThreadLocal как кэш-это не хорошая практика

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

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

Если вы настаиваете, я предлагаю использовать неделю или мягкий ref и выселить после rich maxsize

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


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

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

хотя скорость решений может немного отличаться

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


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

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

помимо используя библиотеку, вы можете взять вдохновение от параллелизм Java на практике листинг 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());
            }
        }
    }
}

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

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

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


скорость поиска, вероятно, аналогична в обоих решениях. Если нет других проблем, я бы предпочел 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;
}

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

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

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