30

问题

我正在通过ExecutorService运行对某些外部方法的多次调用。我希望能够中断这些方法,但不幸的是它们自己不检查中断标志。有什么办法可以强制从这些方法中引发异常?

我知道从任意位置抛出异常是有潜在危险的,在我的具体情况下,我愿意抓住这个机会并准备好应对后果。

细节

“外部方法”是指来自外部库的一些方法,我不能修改它的代码(我可以,但是每当发布新版本时,它都会成为维护的噩梦)。

外部方法计算量大,不受 IO 限制,因此它们不响应常规中断,我无法强制关闭通道或套接字或其他东西。正如我之前提到的,它们也不检查中断标志。

该代码在概念上类似于:

// my code
public void myMethod() {
    Object o = externalMethod(x);
}

// External code
public class ExternalLibrary {
    public Object externalMethod(Object) {
        innerMethod1();
        innerMethod1();
        innerMethod1();
    }

    private void innerMethod1() {
        innerMethod2();
        // computationally intensive operations
    }

    private void innerMethod2() {
        // computationally intensive operations
    }
}

我试过的

Thread.stop()理论上会做我想做的事,但它不仅已被弃用,而且仅适用于实际线程,而我正在处理执行器任务(它也可能与未来的任务共享线程,例如在线程池中工作时) . 尽管如此,如果没有找到更好的解决方案,我会将我的代码转换为使用老式线程并使用这种方法。

我尝试过的另一个选项是myMethod()使用特殊的“可中断”注释标记和类似的方法,然后使用 AspectJ(我承认我是新手)在那里捕获所有方法调用 - 例如:

@Before("call(* *.*(..)) && withincode(@Interruptable * *.*(..))")
public void checkInterrupt(JoinPoint thisJoinPoint) {
    if (Thread.interrupted()) throw new ForcefulInterruption();
}

withincode不是递归匹配方法调用的方法,所以我必须将此注释编辑到外部代码中。

最后,这与我之前的问题类似——尽管一个显着的区别是现在我正在处理一个外部库。

4

8 回答 8

5

我想到了以下奇怪的想法:

  • 使用字节码修改库,例如 Javassist,在字节码的各个点引入中断检查。仅仅在方法的开头可能还不够,因为您提到这些外部方法不是递归的,因此您可能希望在任何时候强制停止它们。在字节码级别执行此操作也会使其响应速度非常快,例如,即使外部代码在循环或其他内容中运行,也可以引入中断检查。但是,这会增加一些开销,因此整体性能会变慢。
  • 为外部代码启动单独的进程(例如单独的虚拟机)。中止进程可能比其他解决方案更容易编码。缺点是您需要在外部代码和您的代码之间建立某种通信通道,例如 IPC、套接字等。第二个缺点是您需要更多的资源(CPU、内存)来启动新的 VM,这可能是特定环境。如果您使用外部代码启动几个任务,而不是数百个任务,这将起作用。此外,性能会受到影响,但计算本身将与原始计算一样快。可以使用 java.lang.Process.destroy() 强制停止进程
  • 使用自定义 SecurityManager,它对每个 checkXXX 方法执行中断检查。如果外部代码以某种方式调用特权方法,那么在这些位置中止可能就足够了。如果外部代码定期读取系统属性,则 java.lang.SecurityManager.checkPropertyAccess(String) 就是一个例子。
于 2010-12-28T23:06:47.763 回答
4

这个解决方案也不容易,但它可以工作:使用 Javassist 或 CGLIB,您可以在每个内部方法(可能由 main run() 方法调用的方法)的开头插入代码以检查线程是否处于活动状态,或其他标志(如果是其他标志,您还必须添加它,以及设置它的方法)。

我建议使用 Javassist/CGLIB 而不是通过代码扩展类,因为您提到它是外部的并且您不想更改源代码,并且将来可能会更改。因此,即使内部方法名称发生变化(或其参数、返回值等),在运行时添加中断检查也适用于当前版本和未来版本。您只需要学习该类并在每个不是 run() 方法的方法的开头添加中断检查。

于 2010-12-28T21:58:50.977 回答
2

一种选择是:

  1. 使用 JDI 使 VM 连接到自身。
  2. 查找正在运行您的任务的线程。这不是微不足道的,但由于您可以访问所有堆栈帧,所以它肯定是可行的。(如果您id在任务对象中放置一个唯一字段,您将能够识别正在执行它的线程。)
  3. 异步停止线程。

尽管我认为停止的线程不会严重干扰执行程序(毕竟它们应该是故障安全的),但还有一种替代解决方案不涉及停止线程。

如果您的任务不修改系统其他部分的任何内容(这是一个合理的假设,否则您不会试图将它们击落),您可以做的是使用 JDI 弹出不需要的堆栈帧并正常退出任务。

public class StoppableTask implements Runnable {

private boolean stopped;
private Runnable targetTask;
private volatile Thread runner;
private String id;

public StoppableTask(TestTask targetTask) {
    this.targetTask = targetTask;
    this.id = UUID.randomUUID().toString();
}

@Override
public void run() {
    if( !stopped ) {
        runner = Thread.currentThread();
        targetTask.run();
    } else {
        System.out.println( "Task "+id+" stopped.");
    }
}

public Thread getRunner() {
    return runner;
}

public String getId() {
    return id;
}
}

这是包装所有其他可运行文件的可运行文件。它存储了对执行线程的引用(稍后会很重要)和一个 id,因此我们可以通过 JDI 调用找到它。

public class Main {

public static void main(String[] args) throws IOException, IllegalConnectorArgumentsException, InterruptedException, IncompatibleThreadStateException, InvalidTypeException, ClassNotLoadedException {
    //connect to the virtual machine
    VirtualMachineManager manager = Bootstrap.virtualMachineManager();
    VirtualMachine vm = null;
    for( AttachingConnector con : manager.attachingConnectors() ) {
        if( con instanceof SocketAttachingConnector ) {
            SocketAttachingConnector smac = (SocketAttachingConnector)con;
            Map<String,? extends Connector.Argument> arg = smac.defaultArguments();
            arg.get( "port" ).setValue( "8000");
            arg.get( "hostname" ).setValue( "localhost" );
            vm = smac.attach( arg );
        }
    }

    //start the test task
    ExecutorService service = Executors.newCachedThreadPool();
    StoppableTask task = new StoppableTask( new TestTask() );
    service.execute( task );
    Thread.sleep( 1000 );

    // iterate over all the threads
    for( ThreadReference thread : vm.allThreads() ) {
        //iterate over all the objects referencing the thread
        //could take a long time, limiting the number of referring
        //objects scanned is possible though, as not many objects will
        //reference our runner thread
        for( ObjectReference ob : thread.referringObjects( 0 ) ) {
            //this cast is safe, as no primitive values can reference a thread
            ReferenceType obType = (ReferenceType)ob.type();
            //if thread is referenced by a stoppable task
            if( obType.name().equals( StoppableTask.class.getName() ) ) {

                StringReference taskId = (StringReference)ob.getValue( obType.fieldByName( "id" ));

                if( task.getId().equals( taskId.value() ) ) {
                    //task with matching id found
                    System.out.println( "Task "+task.getId()+" found.");

                    //suspend thread
                    thread.suspend();

                    Iterator<StackFrame> it = thread.frames().iterator();
                    while( it.hasNext() ) {
                        StackFrame frame = it.next();
                        //find stack frame containing StoppableTask.run()
                        if( ob.equals( frame.thisObject() ) ) {
                            //pop all frames up to the frame below run()
                            thread.popFrames( it.next() );
                            //set stopped to true
                            ob.setValue( obType.fieldByName( "stopped") , vm.mirrorOf( true ) );
                            break;
                        }
                    }
                    //resume thread
                    thread.resume();

                }

            }
        }
    }

}
}

作为参考,我测试了“库”调用:

public class TestTask implements Runnable {

    @Override
    public void run() {
        long l = 0;
        while( true ) {
            l++;
            if( l % 1000000L == 0 )
                System.out.print( ".");
        }

    }
}

Main您可以通过使用命令行选项启动该类来尝试一下-agentlib:jdwp=transport=dt_socket,server=y,address=localhost:8000,timeout=5000,suspend=n。它有两个警告。首先,如果正在执行本机代码(thisObject一帧为空),您必须等到它完成。其次,finally块没有被调用,因此各种资源可能会泄漏。

于 2011-04-05T16:13:29.517 回答
2

你写了:

我尝试过的另一个选项是myMethod()使用特殊的“可中断”注释标记和类似的方法,然后使用 AspectJ(我承认我是新手)在那里捕获所有方法调用 - 例如:

@Before("call(* *.*(..)) && withincode(@Interruptable * *.*(..))")
public void checkInterrupt(JoinPoint thisJoinPoint) {
    if (Thread.interrupted()) throw new ForcefulInterruption();
}

withincode不是递归匹配方法调用的方法,所以我必须将此注释编辑到外部代码中。

AspectJ 的想法很好,但你需要

  • 使用cflow()orcflowbelow()以递归地匹配某个控制流(例如,类似的东西@Before("cflow(execution(@Interruptable * *(..)))"))。
  • 确保还编织您的外部库,而不仅仅是您自己的代码。这可以通过使用二进制编织、检测 JAR 文件的类并将它们重新打包到一个新的 JAR 文件中来完成,或者通过在应用程序启动期间(即在类加载期间)应用 LTW(加载时编织)来完成。

如果您的外部库有一个可以用within(). AspectJ 真的很强大,而且通常有不止一种方法可以解决问题。我会推荐使用它,因为它是为你这样的努力而设计的。

于 2012-11-09T09:43:11.150 回答
1

我为我的问题破解了一个丑陋的解决方案。它不漂亮,但它适用于我的情况,所以我将它发布在这里以防它对其他人有所帮助。

我所做的是分析我的应用程序的库部分,希望我可以隔离一小组重复调用的方法——例如一些get方法equals()或类似这些方面的东西;然后我可以在那里插入以下代码段:

if (Thread.interrupted()) {
    // Not really necessary, but could help if the library does check it itself in some other place:
    Thread.currentThread().interrupt();
    // Wrapping the checked InterruptedException because the signature doesn't declare it:
    throw new RuntimeException(new InterruptedException());
}

通过编辑库的代码手动插入,或者通过编写适当的方面自动插入。请注意,如果库尝试捕获并吞下 a RuntimeException,则抛出的异常可能会替换为库不会尝试捕获的其他异常。

对我来说幸运的是,使用VisualVM,在我对库的特定使用过程中,我能够找到一个调用次数非常多的单一方法。添加上述代码段后,它现在可以正确响应中断。

这当然是不可维护的,而且没有什么能真正保证库在其他情况下会重复调用此方法;但它对我有用,而且因为它相对容易分析其他应用程序并在那里插入检查,我认为这是一个通用的,如果丑陋的解决方案。

于 2011-04-09T08:48:09.007 回答
0

如果内部方法具有相似的名称,那么您可以使用 xml (spring/AspectJ) 中的切入点定义而不是注释,因此不需要对外部库进行代码修改。

于 2010-12-28T09:11:13.480 回答
0

就像mhaller所说,最好的选择是启动一个新流程。由于您的工作不是那么合作,因此您永远无法保证线程终止。

您的问题的一个很好的解决方案是使用支持“轻量级线程”(例如Akka )的任意暂停/停止的库,而不是执行程序服务,尽管这可能有点矫枉过正。

虽然我从未使用过 Akka并且无法确认它是否按预期工作,但文档指出有一个stop()方法用于停止演员。

于 2011-04-05T10:57:22.890 回答
0

据我所知,有两种使用方面的方法

  • 方面
  • 春季AOP

AspectJ 是一个定制的编译器,它拦截正在编译的方法(这意味着它无法获取“外部方法”。Spring AOP(默认情况下)使用类代理在运行时拦截方法(因此它可以拦截“外部方法”。但是问题与 Spring AOP 的区别在于它不能代理已经代理的类(AspectJ 可以这样做,因为它不代理类)。我认为 AspectJ 在这种情况下可以帮助你。

于 2012-11-06T20:50:31.963 回答