ExecutorService - 销毁上下文时无法从ServletContextListener中停止Thread

时间:2014-09-19 11:52:00

标签: java multithreading

我在初始化上下文时从Thread开始ServletContextListener,并在上下文被销毁时尝试停止它。课程是:

public enum BlinkLedTask {

    INSTANCE;

    private Logger logger = RpiLogger.getLogger(getClass());

    private Task task;
    private ExecutorService service;

    private BlinkLedTask() {

    }

    public void run(String[] frequency) {
        stop();

        task = new Task(frequency);
        service = Executors.newSingleThreadExecutor(RpiThreadFactory.INSTANCE);
        service.execute(task);
    }

    public void stop() {
        if(Objects.isNull(task) || Objects.isNull(service)) {
            return;
        }

        try {
            task.terminate();
            service.shutdownNow();
        } catch (Exception cause) {
            logger.error(cause.getMessage(), cause);
        }       
    }

    private static class Task implements Runnable {

        private volatile boolean running = true;
        private String[] frequency;
        private volatile Logger logger = RpiLogger.getLogger(getClass());

        private Task(String[] frequency) {
            this.frequency = frequency;
        }       

        @Override
        public void run() {
            while(running && !Thread.interrupted()) {
                try {
                    resetLed();
                    blinkLed();
                } catch (Throwable cause) {
                    logger.error(cause.getMessage(), cause);
                    running = false;

                    try {
                        resetLed();             
                    } catch (Throwable ignore) {
                    }
                } 
            }
        }

        private void resetLed() throws Exception {
            executeScript(Script.BLINK_LED_RESET);      
        }

        private void blinkLed() throws Exception {
            executeScript(Script.BLINK_LED, new String[]{frequency[0], frequency[1], frequency[2]});        
        }

        private void executeScript(Script script, String... args) {
            ScriptExecutor scriptExecutor = new ScriptExecutor(ScriptExecutor.BASH, script);
            scriptExecutor.execute(true, args);
        }

        private void terminate() {
            logger.info("Stopping - " + Thread.currentThread().getName());
            running = false;
        }
    }
}

这是一个Singleton,它运行一个shell脚本,直到它停止。可以从任何地方调用此类,因此在创建新的Thread之前,如果当前正在执行shell脚本,我需要停止该线程。

出于测试目的,我在初始化上下文时执行了此类的run()方法,并在销毁时调用stop()

我在删除代码run()后重新部署了war文件,我希望stop()会终止task,但事实并非如此。

我也尝试了run()stop()

的不同实现
public void run(String[] frequency) {
    stop();

    task = new Task(frequency);
    Thread thread = RpiThreadFactory.INSTANCE.newThread(task);
    tasks.add(ImmutablePair.of(thread, task));
    thread.start();
}

public void stop() {
    for(ImmutablePair<Thread, Task> pair : tasks) {
        try {
            pair.right.terminate();
            pair.left.join();
        } catch (Exception ex) {

        }           
    }
}

此处tasksprivate ArrayList<ImmutablePair<Thread, Task>> tasks = new ArrayList<ImmutablePair<Thread,Task>>();ImmutablePair属于commons-lang3。但是我在增强的for循环的迭代中收到了java.util.ConcurrentModificationException。我不知道的原因。

更新

当服务器关闭时,stop()正在按预期工作。我正在使用Jetty。

更新

RpiThreadFactory

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.log4j.Logger;

import com.edfx.rpi.app.utils.logger.RpiLogger;

public enum RpiThreadFactory implements ThreadFactory {
    INSTANCE;

    private final AtomicInteger poolNumber = new AtomicInteger(1);
    private final Logger logger = RpiLogger.getLogger(getClass());
    private final ThreadGroup threadGroup;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    private RpiThreadFactory() {
        SecurityManager securityManager = System.getSecurityManager();
        threadGroup = (securityManager != null) ? securityManager.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "RpiPool-" + poolNumber.getAndIncrement() + "-Thread-";

    }

    public Thread newThread(Runnable runnable) {
        Thread thread = new Thread(threadGroup, runnable, namePrefix + threadNumber.getAndIncrement(), 0);
        thread.setPriority(Thread.NORM_PRIORITY);
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {

            public void uncaughtException(Thread thread, Throwable cause) {
                logger.error(cause.getMessage(), cause);
            }
        });

        return thread;
    }
}

ScriptExecutor

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;

import com.edfx.rpi.app.utils.logger.RpiLogger;

public class ScriptExecutor {

    private static final Logger LOGGER = RpiLogger.getLogger(ScriptExecutor.class);
    public static final String BASH = "/bin/bash";

    private Script script;
    private Process process;
    private String output;
    private int exitValue;

    public ScriptExecutor(Script script) {
        this.script = script;

    }

    public void execute(boolean destroyProcess, String... args) throws ScriptNotExistException {                
        if(!script.exists()) {
            throw new ScriptNotExistException(script.getScriptName() + " does not exists.");
        }

        try {
            List<String> commands = new ArrayList<>();

            commands.add(BASH);
            commands.add(script.getAbsoultePath());

            if(Objects.nonNull(args)) {
                commands.addAll(Arrays.asList(args));
            }

            StringBuilder builder = new StringBuilder("Executing script: ");
            builder.append(script.getScriptName());

            if(Objects.nonNull(args) && args.length > 0) {
                builder.append(" with parameters: ");
                builder.append(StringUtils.join(args, " "));
            }

            LOGGER.info(builder.toString());

            ProcessBuilder processBuilder = new ProcessBuilder(commands.toArray(new String[commands.size()]));
            process = processBuilder.start();

            StringBuilder outputBuilder = new StringBuilder();
            InputStream inputStream = process.getInputStream();
            InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

            String line = StringUtils.EMPTY;

            while ((line = bufferedReader.readLine()) != null) {
                outputBuilder.append(line);
                outputBuilder.append("\n");
            }

            process.waitFor();

            exitValue = process.exitValue();
            LOGGER.info("Process for: " + script.getScriptName() + " is executed. Exit value: " + exitValue);

            if(destroyProcess) {
                destroyProcess();
            }

            output = outputBuilder.toString();
        } catch (Exception cause) {
            throw new ScriptExecutionException(cause);
        }       
    }

    public String getOutput() {
        return output;
    }

    public int getExitValue() {
        return exitValue;
    }

    public void destroyProcess() {
        if(Objects.nonNull(process)) {
            LOGGER.info("Process for: " + script.getScriptName() + " is destroyed.");
            process.destroy();
        }
    }
}

目的

这是在Jetty Web容器中运行的Web应用程序。服务器安装在启用嵌入式硬件java中。这个硬件如何连接LED。应用程序接受外部请求,可以是REST并启动LED。因此LED可以根据任何请求开始闪烁;但它一次只能提供一个请求。

这就是为什么我有stop停止以前运行的进程,如果有的话。 stop适用于正常情况。

但是我看到,当LED闪烁并且我在没有停止服务器的情况下进行部署时,正在运行的线程不会停止。如果我停止服务器并进行部署并再次启动,则此时正在运行的线程将被终止。

线程在while中循环,并对本机执行Process。这个Process是一次性工作,所以这个Process并没有让线程被杀死。

为了重现我所做的问题,我在初始化上下文时创建了线程,并在它被销毁时尝试杀死它。现在,如果我在contextDestroyed中写一些东西,我可以看到它们被执行了。

我不明白为什么在重新部署时不会停止服务器杀死线程。

3 个答案:

答案 0 :(得分:3)

您应该在processBuilder.start()返回的Process实例上调用process.destroy()。实际上你在调用BlinkLedTask.terminate()时所做的只是设置一些标志。你应该在这一点上调用process.destroy()。

下面我将举例说明如何重写它。它不涉及你的ScriptExecutor类(当然你可以在那里移动逻辑并在调用blinkLed()时将进程实例返回到BlinkLedTask)。

这里的主要区别在于我在字段blinkLedProcess中保持对Process实例的引用,并且当调用terminate()时,我直接调用process.destroy()来销毁该进程。

您写道“当服务器关闭时,stop()正在按预期工作。我正在使用Jetty。”确实是的。这是因为通过调用processBuilder.start();,您可以创建主要码头流程的子流程。当你杀死码头时,它的所有子潜艇都被杀死了。如果不杀死jetty,则需要通过调用destroy()方法手动终止子进程。

应该是这样的:

public enum BlinkLedTask {
(...)

    private Process resetLedProcess;
    private Process blinkLedProcess;

(...)
   private void blinkLed() throws Exception {
      String[] args = new String[] { frequency[0], frequency[1], frequency[2] };

      List<String> commands = new ArrayList<>();

      //commands.add(BASH);
      commands.add(script.getAbsoultePath());

      if (Objects.nonNull(args)) {
        commands.addAll(Arrays.asList(args));
      }

      StringBuilder builder = new StringBuilder("Executing script: ");
      builder.append(script.getAbsoultePath());

      if (Objects.nonNull(args) && (args.length > 0)) {
        builder.append(" with parameters: ");
        builder.append(StringUtils.join(args, " "));
      }


      ProcessBuilder processBuilder = new ProcessBuilder(commands.toArray(new String[commands.size()]));

      blinkLedProcess = processBuilder.start();

      StringBuilder outputBuilder = new StringBuilder();
      InputStream inputStream = blinkLedProcess.getInputStream();
      InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
      BufferedReader bufferedReader = new BufferedReader(inputStreamReader);

      String line = StringUtils.EMPTY;

      while ((line = bufferedReader.readLine()) != null) {
        outputBuilder.append(line);
        outputBuilder.append("\n");
      }


      blinkLedProcess.waitFor();

      int exitValue = blinkLedProcess.exitValue();
      System.out.println(
        "Process for: " + Script.BLINK_LED.getAbsoultePath() + " is executed. Exit value: " + exitValue);


    }

(...)

   private void terminate() {
      System.out.println("Stopping - " + Thread.currentThread().getName());
      running = false;
      if (resetLedProcess != null) {
        resetLedProcess.destroy();
        System.out.println("Destroyed reset process");
      }
      if (blinkLedProcess != null) {
        blinkLedProcess.destroy();
        System.out.println("Destroyed blink process");
      }
    }
(...)
}

答案 1 :(得分:0)

首先,您应该在致电awaitTermination之后使用shutdownNow等待。

shutdownNow会打断你的线程。你确定ScriptExecutor没有压制中断吗?

这实际上可能是造成这种情况的原因。

使用SchedulerService执行此操作似乎也很多余,因为您只使用一个线程。

您可以启动一个设置为守护程序线程的新线程(请参阅What is Daemon thread in Java?),当程序退出时它将自动关闭。

答案 2 :(得分:0)

你到底想要达到什么目的?如果它是一个在你的web应用程序生命周期内运行的单个线程,那么我只想用这样的线程编写你自己的上下文监听器......

public class MyContextListener implements ServletContextListener {

  Thread myThread;
  Task myTask;

  @Override
  public void contextInitialized(ServletContextEvent sce) {
    myTask = new Task();
    myThread = new Thread(myTask);
    myThread.start();
  }

  @Override
  public void contextDestroyed(ServletContextEvent sce) {
    myTask.terminate();
    myThread.interrupt();
    myThread.join();
  }
}

我想知道你想要实现的目标。你只想要一个运行一系列脚本的线程吗?或者您希望在将来的某个时候有多线程应用程序?

servlet上下文的任何原因?你可以运行这个直接的java应用程序吗?