javamultithreadingexecutorservicefuturetask

Force an ExecutorService thread shutdown in Java to end infinite loop


I'm updating some Java code I wrote several years ago that is part of a larger program for testing Java code written by young students. The idea is similar to JUnit tests but written specifically into a GUI for entry-level coders.

The point of this code segment is to ask Object o to invoke its Method m and return whatever Method m returned. In the event that an exception is thrown, it should return the exception.

My approach is to create a Callable that invokes the provided Method m. I use an Executor to run a FutureTask containing that Callable through a single thread. It works great... unless Method m contains an infinite loop. Then the thread never shuts down, no matter how nicely I ask. You'll see there's some code I previously used (currently commented out) to aggressively force a shutdown, but that isn't supported anymore. It used to work fine. Now that I've commented this thread.stop() section out, the thread containing the student's infinite loop continues to run causing problems for other tests.

I know that the documented approach is to ensure the code in Method m is interruptible, but I'm not in control of what goes into Method m. That method was written by 14-year-olds who are new to coding and it is expected to be full of mistakes (including possible infinite loops.)

Any suggestions?

public static Object getObjectByInvokingMethod(Object o, Method m, Object[] args ) {
   
      ExecutorService es = null;
      FutureTask<?> theTask = null;
      Object returnObject = null;      
   
      try {
         Callable<Object> myCallable =    
               new Callable<Object>()
               {
                  public Object call() {
                     Object returnObject = null;
                     try {
                        returnObject = m.invoke(o, args);              
                     } catch (Exception e) {
                        returnObject = e;
                     }
                     return returnObject;
                  }
               };        
                           
         theTask = new FutureTask<Object>(myCallable);                                          
         es = Executors.newSingleThreadExecutor();
         es.execute(theTask);
         returnObject = theTask.get(5, TimeUnit.SECONDS); //5 second timeout                          
         theTask.cancel(true);
         es.shutdownNow();
         return returnObject;   //Return whatever method m returned (or exception thrown)
     
      } catch (TimeoutException e) {
         if (theTask != null) theTask.cancel(true);
         if (es != null) es.shutdownNow();    
         /* *********  THIS IS WHAT I USED TO DO, BUT DOESN'T WORK IN NEWER JAVA
         try{
            final Field threadField = theTask.getClass().getDeclaredField("runner");
            threadField.setAccessible(true);                    
            Thread t = (Thread)threadField.get(theTask);
            t.stop();
         }catch(Exception e2){
            System.out.print("Problem occurred while forcibly stopping FutureTask thread");
         }
         *************** */
         return e;  //return exception for further handling      
      } catch (Exception e) {
         System.out.print("Something else went wrong.");
         return e;  //return exception for further handling
      }    
   }

Solution

  • The Thread.stop() method was deprecated and later disabled because it could leave shared data in an inconsistent state and corrupt the JVM. That's why your hack no longer works on newer Java versions.

    In Java, you can't stop a thread unless the code inside it checks for Thread.interrupted() and stops itself.

    Instead of using ExecutorService and FutureTask, try another approach - run the code in a separate JVM process using ProcessBuilder. You can set a timeout and, if the code hangs (e.g. infinite loop), just kill the process.

    Something like this:

    ProcessBuilder pb = new ProcessBuilder("java", "StudentCodeRunner");
    Process process = pb.start();
    
    boolean finished = process.waitFor(10, TimeUnit.SECONDS);
    if (!finished) {
        process.destroyForcibly();
    }
    
    

    If you keep using threads, you can detect timeouts and treat them as test failures, but you can't safely stop the thread, it will just stay running in the background.