web-dev-qa-db-fra.com

Meilleures pratiques de Java Executor pour les tâches à exécuter pour toujours

Je travaille sur un projet Java dans lequel plusieurs tâches doivent être exécutées de manière asynchrone. Je suis amené à croire que l'exécuteur est la meilleure façon pour moi de faire cela, alors je me familiarise avec cela. (Yay être payé pour apprendre!) Cependant, il n'est pas clair pour moi quel est le meilleur moyen d'accomplir ce que j'essaie de faire.

Par souci d'argumentation, disons que deux tâches sont en cours d'exécution. Aucune des deux ne doit se terminer et les deux doivent s'exécuter pendant la durée de vie de l'application. J'essaie d'écrire une classe de wrapper principale telle que:

  • Si l'une des tâches lève une exception, l'encapsuleur la détecte et la redémarre.
  • Si l'une des tâches est complétée, l'encapsuleur le remarquera et le redémarrera.

A présent, il convient de noter que l'implémentation des deux tâches encapsulera le code dans run() dans une boucle infinie qui ne s'exécutera jamais complètement, avec un bloc try/catch qui devrait gérer toutes les exceptions d'exécution sans interrompre la boucle. J'essaie d'ajouter une autre couche de certitude; Si moi-même ou quelqu'un qui me suit fait quelque chose de stupide qui contrecarre ces garanties et arrête la tâche, l'application doit réagir de manière appropriée.

Existe-t-il une meilleure pratique pour aborder ce problème que des personnes plus expérimentées que je ne le recommanderais? 

FWIW, j'ai préparé cette classe de test:


public class ExecTest {

   private static ExecutorService executor = null;
   private static Future results1 = null;
   private static Future results2 = null;

   public static void main(String[] args) {
      executor = Executors.newFixedThreadPool(2);
      while(true) {
         try {
            checkTasks();
            Thread.sleep(1000);
         }
         catch (Exception e) {
            System.err.println("Caught exception: " + e.getMessage());
         }
      }
   }

   private static void checkTasks() throws Exception{
      if (results1 == null || results1.isDone() || results1.isCancelled()) {
         results1 = executor.submit(new Test1());
      }

      if (results2 == null || results2.isDone() || results2.isCancelled()) {
         results2 = executor.submit(new Test2());
      }
   }
}

class Test1 implements Runnable {
   public void run() {
      while(true) {
         System.out.println("I'm test class 1");
         try {Thread.sleep(1000);} catch (Exception e) {}
      }

   }
}

class Test2 implements Runnable {
   public void run() {
      while(true) {
         System.out.println("I'm test class 2");
         try {Thread.sleep(1000);} catch (Exception e) {}
      }
   }
}

Il se comporte comme je le veux, mais je ne sais pas s'il y a des pièges, des inefficacités ou des manœuvres irréfléchies qui attendent de me surprendre. (En fait, étant donné que je suis nouveau dans ce domaine, je serais choqué si n'était pas quelque chose de mal/déconseillé à ce sujet.)

Toute idée est la bienvenue.

36
BlairHippo

Je faisais face à une situation similaire dans mon projet précédent, et après que mon code soit apparu devant un client en colère, mes copains et moi avons ajouté deux grands protections:

  1. Dans la boucle infinie, attrapez aussi les erreurs, pas seulement les exceptions. Parfois, des choses inacceptées se produisent et Java vous envoie une erreur, pas une exception.
  2. Utilisez un interrupteur de protection. Ainsi, si quelque chose ne va pas et n’est pas récupérable, vous ne dégénérez pas la situation en démarrant avec impatience une autre boucle. Au lieu de cela, vous devez attendre que la situation redevienne normale, puis recommencer.

Par exemple, nous avons eu une situation où la base de données était en panne et lors de la boucle, une exception SQLException a été émise. Le résultat regrettable est que le code a refait une boucle, pour ensuite atteindre la même exception, et ainsi de suite. Les journaux ont montré que nous avons frappé la même SQLException environ 300 fois en une seconde! ... cela s'est produit plusieurs fois par intermittence avec des pauses occasionnelles de 5 secondes environ de la machine virtuelle Java, pendant lesquelles l'application ne répondait pas, jusqu'à ce qu'une erreur soit finalement générée et que le thread soit mort!

Nous avons donc implémenté une stratégie de secours, illustrée approximativement dans le code ci-dessous, selon laquelle si l'exception n'est pas récupérable (ou est récupérée en quelques minutes), nous attendons plus longtemps avant de reprendre les opérations.

class Test1 implements Runnable {
  public void run() {
    boolean backoff = false;
    while(true) {
      if (backoff) {
        Thread.sleep (TIME_FOR_LONGER_BREAK);
        backoff = false;
      }
      System.out.println("I'm test class 1");
      try {
        // do important stuff here, use database and other critical resources
      }
      catch (SqlException se) {
       // code to delay the next loop
       backoff = true;
      }
      catch (Exception e) {
      }
      catch (Throwable t) {
      }
    }
  }
}

Si vous implémentez vos tâches de cette façon, je ne vois pas l'intérêt d'avoir un troisième thread "watch-dog" avec la méthode checkTasks (). En outre, pour les mêmes raisons que celles évoquées ci-dessus, je ferais bien de recommencer la tâche avec l'exécuteur. Vous devez d’abord comprendre pourquoi la tâche a échoué et si l’environnement est dans un état stable, il serait utile de réexécuter la tâche.

31
Yoni

En plus de l'observer, j'utilise généralement du code Java contre des outils d'analyse statiques tels que PMD et FindBugs pour rechercher des problèmes plus profonds. 

En particulier pour ce code, FindBugs n'a pas apprécié que results1 et results2 ne soient pas volatils dans l'init paresseux et que les méthodes run () puissent ignorer l'exception, car elles ne sont pas explicitement gérées. 

En général, je suis un peu méfiant quant à l'utilisation de Thread.sleep pour les tests de simultanéité, préférant les temporisateurs ou les états/conditions de terminaison. Callable peut être utile pour renvoyer quelque chose en cas de perturbation qui lève une exception s'il est impossible de calculer un résultat. 

Pour des meilleures pratiques et davantage de matière à réflexion, consultez La concurrence en pratique

7
David Sowsy

avez-vous essayé Quartz framework ?

0

que dis-tu de ça 

Runnable task = () -> {
  try{
    // do the task steps here
  } catch (Exception e){
    Thread.sleep (TIME_FOR_LONGER_BREAK);
  }    
};
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(task,0, 0,TimeUnit.SECONDS);
0
deepak