6

遗憾的是,在 Java 中对字符串使用正则表达式时无法指定超时。因此,如果您没有严格控制将哪些模式应用于哪些输入,您最终可能会拥有消耗大量 CPU 的线程,同时无休止地尝试将(设计不那么好的)模式与(恶意?)输入进行匹配。

我知道不推荐使用 Thread#stop() 的原因(请参阅http://download.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html)。它们以在 ThreadDeath 异常的情况下可能会损坏的对象为中心,然后污染您正在运行的 JVM 环境并可能导致细微的错误。

对于比我更深入了解 JVM 工作原理的任何人,我的问题是:如果需要停止的线程没有任何(明显的)监视器或对程序其余部分使用的对象的引用,那么使用 Thread#stop() 是否可以接受?

我创建了一个相当防御性的解决方案,以便能够处理带有超时的正则表达式匹配。我很乐意发表任何评论或评论,尤其是关于这种方法可能导致的问题,尽管我努力避免这些问题。

谢谢!

import java.util.concurrent.Callable;

public class SafeRegularExpressionMatcher {

    // demonstrates behavior for regular expression running into catastrophic backtracking for given input
    public static void main(String[] args) {
        SafeRegularExpressionMatcher matcher = new SafeRegularExpressionMatcher(
                "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "(x+x+)+y", 2000);
        System.out.println(matcher.matches());
    }

    final String stringToMatch;

    final String regularExpression;

    final int timeoutMillis;

    public SafeRegularExpressionMatcher(String stringToMatch, String regularExpression, int timeoutMillis) {
        this.stringToMatch = stringToMatch;
        this.regularExpression = regularExpression;
        this.timeoutMillis = timeoutMillis;
    }

    public Boolean matches() {
        CallableThread<Boolean> thread = createSafeRegularExpressionMatchingThread();
        Boolean result = tryToGetResultFromThreadWithTimeout(thread);
        return result;
    }

    private CallableThread<Boolean> createSafeRegularExpressionMatchingThread() {
        final String stringToMatchForUseInThread = new String(stringToMatch);
        final String regularExpressionForUseInThread = new String(regularExpression);
        Callable<Boolean> callable = createRegularExpressionMatchingCallable(stringToMatchForUseInThread,
                regularExpressionForUseInThread);
        CallableThread<Boolean> thread = new CallableThread<Boolean>(callable);
        return thread;
    }

    private Callable<Boolean> createRegularExpressionMatchingCallable(final String stringToMatchForUseInThread,
            final String regularExpressionForUseInThread) {
        Callable<Boolean> callable = new Callable<Boolean>() {
            public Boolean call() throws Exception {
                return Boolean.valueOf(stringToMatchForUseInThread.matches(regularExpressionForUseInThread));
            }
        };
        return callable;
    }

    private Boolean tryToGetResultFromThreadWithTimeout(CallableThread<Boolean> thread) {
        startThreadAndApplyTimeout(thread);
        Boolean result = processThreadResult(thread);
        return result;
    }

    private void startThreadAndApplyTimeout(CallableThread<Boolean> thread) {
        thread.start();
        try {
            thread.join(timeoutMillis);
        } catch (InterruptedException e) {
            throwRuntimeException("Interrupt", e);
        }
    }

    private Boolean processThreadResult(CallableThread<Boolean> thread) {
        Boolean result = null;
        if (thread.isAlive()) {
            killThread(thread); // do not use anything from the thread anymore, objects may be damaged!
            throwRuntimeException("Timeout", null);
        } else {
            Exception exceptionOccurredInThread = thread.getException();
            if (exceptionOccurredInThread != null) {
                throwRuntimeException("Exception", exceptionOccurredInThread);
            } else {
                result = thread.getResult();
            }
        }
        return result;
    }

    private void throwRuntimeException(String situation, Exception e) {
        throw new RuntimeException(situation + " occured while applying pattern /" + regularExpression + "/ to input '"
                + stringToMatch + " after " + timeoutMillis + "ms!", e);
    }

    /**
     * This method uses {@link Thread#stop()} to kill a thread that is running wild. Although it is acknowledged that
     * {@link Thread#stop()} is inherently unsafe, the assumption is that the thread to kill does not hold any monitors on or
     * even references to objects referenced by the rest of the JVM, so it is acceptable to do this.
     * 
     * After calling this method nothing from the thread should be used anymore!
     * 
     * @param thread Thread to stop
     */
    @SuppressWarnings("deprecation")
    private static void killThread(CallableThread<Boolean> thread) {
        thread.stop();
    }

    private static class CallableThread<V> extends Thread {

        private final Callable<V> callable;

        private V result = null;

        private Exception exception = null;

        public CallableThread(Callable<V> callable) {
            this.callable = callable;
        }

        @Override
        public void run() {
            try {
                V result = compute();
                setResult(result);
            } catch (Exception e) {
                exception = e;
            } catch (ThreadDeath e) {
                cleanup();
            }
        }

        private V compute() throws Exception {
            return callable.call();
        }

        private synchronized void cleanup() {
            result = null;
        }

        private synchronized void setResult(V result) {
            this.result = result;
        }

        public synchronized V getResult() {
            return result;
        }

        public synchronized Exception getException() {
            return exception;
        }

    }

}

编辑:

感谢 dawce 向我指出了这个解决方案,我能够在不需要额外线程的情况下解决我原来的问题。我已经在那里发布了代码。感谢所有回复的人。

4

4 回答 4

9

如果您确定它是唯一可用的解决方案,则可以使用 Thread.stop()。您可能需要关闭并重新启动应用程序以确保其处于良好状态。

注意:线程可以捕获和忽略ThreadDeath,因此不保证停止所有线程。

停止线程的另一种方法是在不同的进程中运行它。这可以根据需要杀死。这仍然会使资源处于不一致的状态(如锁定文件),但不太可能且更容易控制。

最好的解决方案当然是修复代码,使其一开始就不会这样做,而是尊重 Thread.interrupt() 。

于 2012-07-03T09:07:05.003 回答
2

如果需要停止的线程没有任何(明显的)监视器或对程序其余部分使用的对象的引用,那么仍然可以使用 Thread#stop() 吗?

是否“可接受”由您决定。我们所能做的就是建议它是否安全。答案是它不是。

  • 它拥有的非显而易见的监视器和引用呢?

  • 否则它会发出通知等呢?

  • 否则它可能会影响静力学的动作呢?

问题是很难(在大多数情况下)确定您已经考虑了线程可能与应用程序的其余部分进行的所有可能的交互。


重新启动应用程序正是我试图避免的......

令我震惊的是,是您问题的真正根源;即,您在设计程序时没有考虑到出于实际原因需要重新启动长时间运行的程序这一事实。尤其是那些有潜在错误的复杂的。

于 2012-07-03T09:59:46.380 回答
2

而不是使用Thread.stop()which 已被弃用,使用Thread.interrupt()which 将停止引发中断标志,可以通过isInterrupted()or进行检查interrupted(),或者抛出一个InterruptedException.

我构建扩展 Thread 类的模式是这样的

 class MyThread extends Thread{
      private volatile boolean keepRunning = true;

      public void run(){
           while(keepRunning){
                // do my work
           }
       }

       public void killThread(){
           keepRunning = false;
           this.interrupt();
       }
 }

我并不是说我的处理方式是完美的,可能会有更好的选择,但这对我有用。

于 2012-07-03T09:05:33.327 回答
1

如果您专门将线程代码设计为不持有锁等,(是的,这包括非显式锁。例如,可以在更改字符串大小时使用的 malloc 锁),然后停止线程,是的。轮询“中断”标志很好,除了它意味着轮询“中断”标志,即。在 99.9999% 的时间内,它实际上并未设置。这可能是高性能、紧密循环的问题。

如果检查可以保持在最内层循环之外并且仍然可以合理地频繁检查,那么这确实是最好的方法。

如果无法经常检查该标志(例如,由于无法访问的库代码中的紧密循环),您可以将线程优先级设置为尽可能低的优先级并忘记它,直到它最终死掉。

有时可能的另一个问题是破坏线程正在其上工作的数据,使库代码正常退出,导致引发异常,因此控制从不透明的库代码中冒出或导致“OnError” ' 要调用的处理程序。如果库。正在对字符串进行操作,用空值喷出字符串肯定会有所作为。任何例外都可以 - 如果您可以在线程中安排 AV/segfault,那么只要您重新获得控制权就可以了。

于 2012-07-03T11:34:23.157 回答