web-dev-qa-db-fra.com

Est-il dangereux d'utiliser ThreadLocal avec ExecutorService?

Je parcourais le concept de ThreadLocals sur le blog ci-dessous:

https://www.baeldung.com/Java-threadlocal

Il dit que "N'utilisez pas ThreadLocal avec ExecutorService"

Il illustre l'exemple ci-dessous pour l'utilisation de ThreadLocals.

public class ThreadLocalWithUserContext implements Runnable {

    private static ThreadLocal<Context> userContext 
      = new ThreadLocal<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContext.set(new Context(userName));
        System.out.println("thread context for given userId: "
          + userId + " is: " + userContext.get());
    }

    // standard constructor
}

À la fin de l'article, il est mentionné que:

Si nous voulons utiliser un ExecutorService et lui soumettre un Runnable, l'utilisation de ThreadLocal produira des résultats non déterministes - car nous n'avons pas la garantie que chaque action Runnable pour un ID utilisateur donné sera gérée par le même thread à chaque exécution. .

Pour cette raison, notre ThreadLocal sera partagé entre différents ID utilisateur. C'est pourquoi nous ne devons pas utiliser un TheadLocal avec ExecutorService. Il ne doit être utilisé que lorsque nous avons un contrôle total sur le thread qui choisira l'action exécutable à exécuter.

Cette explication a été un videur pour moi. J'ai essayé de faire des recherches en ligne sur ce point en particulier, mais je n'ai pas pu obtenir beaucoup d'aide, un expert peut-il développer les explications ci-dessus? Est-ce le point de vue des auteurs ou une véritable menace?

10
Hitesh

ThreadLocal est utilisé lorsque vous souhaitez mettre en cache une variable dans ce thread après que vous l'ayez définie. Donc, la prochaine fois que vous voulez y accéder, vous pouvez simplement l'obtenir directement depuis ThreadLocal sans initialisation.

Puisque vous le définissez avec threadLocal.set(obj) et que vous y accédez via threadLocal.get() dans un thread, vous avez donc directement une garantie thread-safe.

Mais les choses pourraient devenir laides, si vous n'effacez pas explicitement le cache par threadLocal.remove().

  1. dans un pool de threads, les tâches mises en file d'attente seront traitées par les threads une par une et la tâche devrait être indépendante la plupart du temps, mais le cache de thread-portée threadLocal fera dépendre les tâches suivantes de ses précédentes si vous oubliez de l'effacer avant de traiter la tâche suivante;

  2. le mis en cache threadLocals ne sera pas gc-ed immédiatement (à un moment inconnu moment - hors de votre contrôle) puisque leur les clés sont WeakReference , ce qui pourrait provoquer un MOO à votre insu.

Une démonstration simple pour un tel cas où remove() n'est pas explicitement invoqué provoquant un MOO.

public class ThreadLocalOne {
    private static final int THREAD_POOL_SIZE = 500;
    private static final int LIST_SIZE = 1024 * 25;

    private static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

        for (int i = 0; i < THREAD_POOL_SIZE; i++) {
            executorService.execute(() -> {
                threadLocal.set(getBigList());
                System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get().size());
                // threadLocal.remove(); 
                // explicitly remove the cache, OOM shall not occur;
            });
        }
        executorService.shutdown();
    }

    private static List<Integer> getBigList() {
        List<Integer> ret = new ArrayList<>();
        for (int i = 0; i < LIST_SIZE; i++) {
            ret.add(i);
        }
        return ret;
    }
}
0
Hearen