如何使外部方法可以中断?

时间:2010-12-28 06:29:21

标签: java multithreading aop aspectj executorservice

问题

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

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

详细

“外部方法”我的意思是来自外部库的一些方法,我无法修改它的代码(我可以,但是每当新版本发布时,这将使它成为维护的噩梦)。 / p>

外部方法计算成本高,不受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()理论上会做我想要的,但它不仅被弃用,而且它也只适用于实际线程,而我正在使用执行器任务(也可能与未来的任务共享线程,在线程池中工作时的示例)。然而,如果没有找到更好的解决方案,我会将我的代码转换为使用老式的Threads并使用此方法。

我尝试的另一个选项是使用特殊的“Interruptable”注释来标记myMethod()和类似方法,然后使用AspectJ(我无疑是新手)来捕获所有方法调用 - 例如:

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

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

最后,这与a previous question of mine类似 - 虽然一个明显的区别是现在我正在处理外部库。

8 个答案:

答案 0 :(得分:5)

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

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

答案 1 :(得分:4)

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

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

答案 2 :(得分:2)

选项是:

  1. Make the VM connect to itself using JDI.
  2. 查找正在运行任务的主题。这并非易事,但由于您可以访问所有堆栈帧,因此它肯定是可行的。 (如果在任务对象中放置一个唯一的id字段,则可以识别正在执行它的线程。)
  3. Stop the thread asynchronously.
  4. 虽然我认为停止的线程不会严重干扰执行程序(毕竟它们应该是故障安全的),但是有一种替代解决方案不涉及停止线程。

    如果您的任务不修改系统其他部分的任何内容(这是公平的假设,否则您不会尝试将其击落),您可以做的是使用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;
    }
    }
    

    这是包含所有其他runnable的runnable。它存储对执行线程的引用(稍后将很重要)和一个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块,因此可能会泄漏各种资源。

答案 3 :(得分:2)

您写道:

  

我尝试过的另一个选择是标记myMethod()和类似的方法   使用特殊的“Interruptable”注释然后使用AspectJ(其中   我无疑是在那里捕获所有方法调用的新手    - 类似于:

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

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

AspectJ的想法很好,但你需要

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

如果您的外部库具有可以使用within()查明的包名称,则可能甚至不需要您的标记注释。 AspectJ非常强大,通常有多种方法可以解决问题。我建议使用它,因为它是为了你的努力而制作的。

答案 4 :(得分:1)

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

我所做的是 profile 我的应用程序的库部分,希望我可以隔离一小组重复调用的方法 - 例如一些get方法或{{1或者沿着这些方向的东西;然后我可以在那里插入以下代码段:

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()); } ,则抛出的异常可以替换为库不会尝试捕获的其他内容。

幸运的是,使用VisualVM,我能够在我制作库的具体用法中找到一个非常多次的方法。添加上面的代码段后,它现在可以正确响应中断。

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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

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

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

答案 7 :(得分:0)

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

  • AspecJ
  • Spring AOP

AspectJ是一个自定义编译器,它拦截编译的方法(意味着它无法获得“外部方法”)。 Spring AOP(默认情况下)使用类代理在运行时拦截方法(因此它可以拦截“外部方法”。但Spring AOP的问题是它不能代理已经代理的类(AspectJ可以做什么,因为它没有代理类)。我认为AspectJ可以帮助你。