使用线程池将紧密耦合方法转换为松散耦合的优雅方法

时间:2012-09-13 21:49:07

标签: java multithreading concurrency

  1. 我无法想出一个更好的标题,所以会欣赏编辑。
  2. 我遇到的问题是,库中暴露的许多方法需要花费大量时间,因为它们会进行大量的工作或阻止I / O ...例如,当我要求Solr索引文档时。 / LI>
  3. 因此,要解决这个问题,我最终要做的是编写包装代码,它实际上是在Thread的run()方法中调用方法。我踢了工作线程来实现并行性,然后提取结果并将它们与原始调用进行对比。
  4. 我突然意识到有人可能想出一个优雅的方法来做到这一点,基本上:

    1. 采取方法
    2. 获取某种Factory方法将其转换为异步调用
    3. 另外围绕这个包装一个ExecutorService,这样你就可以为我们调用这些调用,并让所有工作线程都通过它来实现。
  5. 假设我有一个类:

    public class SomeUtilClass {
    
      public int someComplicatedTask(int x, int y, int z) {
        // some very very complicated IO CPU bound task
      }
    
      // other equally lifting methods go here
    }
    

    是否有一种优雅的方法可以将线程池​​和任务队列放在每个方法的前面。

    基本上我做的是这样的事情:

    wrappedObject = EncapsulateWithThreadPoolAndTaskQueueFactory.wrap(SomeUtilClass,
        10 /*worker threads*/);
    
    String ticket1=wrappedObject.asyncExecSomeComplicatedTask(1,2,3);
    String ticket2=wrappedObject.asyncExecSomeComplicatedTask(4,5,6);
    String ticket2=wrappedObject.asyncExecSomeComplicatedTask(7,8,9);
    
    wrappedObject.joinOnTicket(ticket1); 
    int result1=wrappedObject.getRestTicket(ticket1);
    

    依此类推......

3 个答案:

答案 0 :(得分:2)

我们之前使用过TaskFactory系统回调。也就是说,我们将它用于请求和响应,但它可以用于许多事情。基本上,您将拥有TaskFactory界面:

public interface TaskFactory<T, R> {
    Callable<R> taskFor(T t);
}

public interface Callback<R> {
    void onCallback(R r);
    void onException(Throwable t);
}

对于请求/响应内容,我们使用TaskFactory<Request, Response>的实例为Callable对象提供Request个实例,这些对象执行网络I / O并返回Response

public class RequestTaskFactory implements TaskFactory<Request, Response> {
    public Callable<Response> taskFor(Request request) {
        return new RequestTask(request);
    }
}

任务:

class RequestTask implements Callable<Response> {
    private final Request request;

    public RequestTask(Request request) {
        this.request = request;
    }

    public Response call() {
        // Open socket
        // Send request
        // Receive response
        Response resp = getResponse(socket);
        return resp;
    }
}

最后一个接口和类将它们组合在一起:

public interface TaskHandler<T, R> {

    TaskFactory<T, R> getTaskFactory();

    Future<R> submit(T t);

    void submit(T t, Callback<R> callback);
}

如果您愿意,还可以使用另一个方法在事件派发线程上使用SwingUtilities.invokeLater执行回调,以防您完全使用Swing。实施:

public class TaskHandlerImpl<T, R> implements TaskHandler<T, R> {

    private final ExecutorService executor;
    private final TaskFactory<T, R> taskFactory;

    public TaskHandlerImpl(TaskFactory<T, R> taskFactory, int threadPoolSize) {
        this.taskFactory = taskFactory;
        this.executor = Executors.newFixedThreadPool(threadPoolSize);
    }

    public TaskFactory<T, R> getTaskFactory() {
        return taskFactory;
    }

    public Future<R> submit(T t) {
        return executor.submit(taskFactory.taskFor(t));
    }

    public void submit(T t, Callback<R> callback) {
        executor.execute(new CallbackTask(taskFactory.taskFor(t)
    }

    private static class CallbackTask<R> implements Runnable {
        private final Callback<R> callback;
        private final Callable<R> callable;

        CallbackTask(Callback<R> callback, Callable<R> callable) {
            this.callback = callback;
            this.callable = callable;
        }

        public void run() {
            try {
                callback.onCallback(callable.call());
            } catch (Exception ex) {
                callback.onException(ex);
            }
        }
    }
}

然后,这只是编写TaskFactoryCallableCallback实现的问题。

答案 1 :(得分:2)

除了我更通用的并发答案之外的另一个选择是使用Java的Proxy类。

假设您有一个类Foo,并且您希望将其包装起来,以便它将在后台线程中执行所有方法。它有一个方法doSomething需要很长时间才能执行。

public class Foo {

    public void doSomething() {
        // network I/O or something
        Thread.sleep(5000);
    }
}

要使用Proxy,您需要一个界面。 (请参阅this tutorial。)从Foo创建一个界面:

public interface Foo {
    void doSomething();
}

让您的旧Foo课程实现您的新界面:

public class FooImpl implements Foo { ... }

Java的Proxy需要做两件事:接口(得到那个)和InvocationHandlerInvocationHandler将执行后台线程中的方法。

以下是我们如何使用Proxy的实例创建FooImpl

Foo foo = new FooImpl();
foo = (Foo) Proxy.newInstance(
        System.getClassLoader(),
        new Class[] { Foo.class },
        new BackgroundInvocationHandler(foo));

非常简单,真的。现在我们只需要写BackgroundInvocationHandler

public class BackgroundInvocationHandler implements InvocationHandler {

    private static final int THREAD_COUNT = 20;
    private static final Executor executor = Executors.newFixedThreadPool(THREAD_COUNT);

    private final Object obj;

    public BackgroundInvocationHandler(Object obj) {
        this.obj = obj;
    }

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        executor.execute(new MethodInvoker(m, args, obj));
        return null;
    }

    private static class MethodInvoker implements Runnable {

        private final Method m;
        private final Object[] args;
        private final Object target;

        MethodInvoker(Method m, Object[] args, Object target) {
            this.m = m;
            this.args = args;
            this.target = target;
        }

        @Override
        public void run() {
            try {
                m.invoke(target, args);
            } catch(Exception e) {
                // TODO log me
            }
        }
    }
}

正如我相信你可能期望的那样,这只适用于返回void的方法。对于返回不是Proxy的内容的任何内容,null实例都会返回void。但是,您始终可以将此与回调或事件分派相结合,以从这些内容中获取结果。请记住,这就像提交作业并离开它们一样,因此如果不实现某种回调系统,您将无法获得任何回报。

还有一句话:如果你想确保所有你的对象被包装,那么你需要一个工厂来创建Foo个对象:

public class FooFactory {
    public static Foo createFoo() {
        Foo foo = new FooImpl();
        foo = (Foo) Proxy.newInstance(
                System.getClassLoader(),
                new Class[] { Foo.class },
                new BackgroundInvocationHandler(foo));
        return foo;
    }
}

然后将FooImpl包私有,这样包外的任何人都不能通过从类声明中删除public来访问它:

class FooImpl implements Foo { ... }

你去吧。这是使用简单方法调用将对象包装为后台调用的好方法。此代码将在后台执行doSomething并立即打印给定语句:

Foo myFoo = FooFactory.createFoo();
myFoo.doSomething();
System.out.println("Prints immediately without waiting");

与我的其他答案相比,这更接近你想要的东西,但由于返回类型的要求,它更受限制。

答案 2 :(得分:1)

而不是各种(可能是多个)客户端类对代码进行小包装,我更喜欢实现方法的Runnable或Callable版本,以实现它们,以鼓励用户(通过制作它)容易)异步使用它们。显然,这意味着您预计它们将被异步使用。你甚至可以传递参数。 e.g。

public class Foo {

   public String someLongMethodA(Object...args) {
      // code that takes a long time here
      return someResult;
   }

   public Callable<String> callableLongMethodA(final Object...args) {
      return new Callable<String>() {
         @Override
         public String call() throws Exception {
            return someLongMethodA(args);
         }

      };
   }


   public Integer someLongMethod2(int loopCount) {
      // yet more long running code here
      return 42;  // the meaning of life
   }

   public Callable<Integer> callableLongMethod2(final int loopCount) {
      return new Callable<Integer>() {
         @Override
         public Integer call() throws Exception {
            return someLongMethod2(loopCount);
         }         
      };
   }  


}

这远非完美,因为您正在编写大量的样板代码。但这就是Eclipse模板的用途。此外,在某些情况下,您必须小心,客户在进行初始调用后不会更改参数。