Carte de blocage par clé en Java


J'ai affaire à un code de bibliothèque tiers qui implique de créer des objets coûteux et de les mettre en cache dans un Map. L'implémentation existante est quelque chose comme

lock.lock()
try {
    Foo result = cache.get(key);
    if (result == null) {
        result = createFooExpensively(key);
        cache.put(key, result);
    }
    return result;
} finally {
    lock.unlock();
}

Évidemment, ce n'est pas le meilleur design lorsque Foos pour différents keys peut être créé indépendamment.

Mon hack est d'utiliser un Map de Futures:

lock.lock();
Future<Foo> future;
try {
    future = allFutures.get(key);
    if (future == null) {
        future = executorService.submit(new Callable<Foo>() {
            public Foo call() {
                return createFooExpensively(key);
            }
        });
        allFutures.put(key, future);
    }
} finally {
    lock.unlock();
}

try {
    return future.get();
} catch (InterruptedException e) {
    throw new MyRuntimeException(e);
} catch (ExecutionException e) {
    throw new MyRuntimeException(e);
}

Mais cela semble... un peu hacky, pour deux raisons:

  1. Le travail est effectué sur un thread groupé arbitraire. Je serais heureux d'avoir du travail fait le premier thread qui tente d'obtenir cette clé, surtout depuis il va être bloqué de toute façon.
  2. Même lorsque le Map est entièrement rempli, nous passons toujours par Future.get() pour obtenir résultat. Je m'attends à ce que ce soit assez bon marché, mais c'est moche.

Ce que j'aimerais, c'est remplacer cache par un Mapqui bloquera gets pour une clé donnée jusqu'à ce que cette clé ait une valeur, mais autorisera d'autres gets pendant ce temps. N'a aucune une telle chose existe? Ou quelqu'un vous avez une alternative plus propre au Map de Futures?

Author: David Moles, 2013-05-29

2 answers

Créer un verrou par touche semble tentant, mais ce n'est peut-être pas ce que vous voulez, surtout lorsque le nombre de touches est important.

Comme vous auriez probablement besoin de créer un verrou dédié (lecture-écriture) pour chaque clé, cela a un impact sur votre utilisation de la mémoire. En outre, cette granularité fine peut atteindre un point de rendement décroissant compte tenu d'un nombre fini de cœurs si la concurrence est vraiment élevée.

ConcurrentHashMap est souvent une solution assez bonne dans une situation comme celle-ci. Il fournit normalement simultanéité complète du lecteur (normalement, les lecteurs ne bloquent pas) et les mises à jour peuvent être simultanées jusqu'au niveau de simultanéité souhaité. Cela vous donne une assez bonne évolutivité. Le code ci-dessus peut être exprimé avec ConcurrentHashMap comme suit:

ConcurrentMap<Key,Foo> cache = new ConcurrentHashMap<>();
...
Foo result = cache.get(key);
if (result == null) {
  result = createFooExpensively(key);
  Foo old = cache.putIfAbsent(key, result);
  if (old != null) {
    result = old;
  }
}

L'utilisation simple de ConcurrentHashMap a un inconvénient, à savoir que plusieurs threads peuvent trouver que la clé n'est pas mise en cache, et chacun peut invoquer createFooExpensively(). En conséquence, certains threads peuvent faire un travail de jeter. Pour éviter cela, vous souhaitez utiliser le modèle memoizer mentionné dans "Java Concurrency in Practice".

Mais là encore, les gens sympas de Google ont déjà résolu ces problèmes pour vous sous la forme deCacheBuilder :

LoadingCache<Key,Foo> cache = CacheBuilder.newBuilder().
  concurrencyLevel(32).
  build(new CacheLoader<Key,Foo>() {
    public Foo load(Key key) {
      return createFooExpensively(key);
    }
  });

...
Foo result = cache.get(key);
 7
Author: sjlee, 2018-03-26 10:56:50

Vous pouvez utiliser funtom-java-utils - PerKeySynchronizedExecutor.

, Il va créer un verrou pour chaque clé, mais sera clair pour vous immédiatement quand il devient inutilisés.

Il accordera également une visibilité de la mémoire entre les appels avec la même clé, et est conçu pour être très rapide et minimiser les conflits entre les appels sur différentes clés.

Déclarez-le dans votre classe:

final PerKeySynchronizedExecutor<KEY_CLASS> executor = new PerKeySynchronizedExecutor<>();

Utilisez-le:

Foo foo = executor.execute(key, () -> createFooExpensively());
 2
Author: Oz Molaim, 2016-07-17 13:11:25