Java - разбиение на несколько потоков

У меня возникла следующая проблема: мне нужно разделить работу между несколькими потоками по причинам производительности, но я не уверен, какой подход выбрать.

Во-первых, поставленная мною задача должна вернуть значение и принять параметр. Кроме того, основной метод (выполняющий основную часть работы, а не static main()) уже выполняется в отдельном потоке и вызывается периодически. Кроме того, этот метод должен в какой-то момент дождаться завершения всех потоков, а затем продолжить.

Один из подходов (наиболее очевидный для меня) состоит в том, чтобы планировать каждую работу в отдельном потоке и сохранять результаты в классе:

public Object result1, result2;

public void mainMethod() throws InterruptedException {
    final Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            result1 = expensiveMethod("param1");
        }
    });

    final Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            result2 = expensiveMethod("param2");
        }
    });

    thread1.join();
    thread.join();

    //Do rest of work
}

private Object expensiveMethod(Object param){
    // Do work and return result
}

Это немного уродливо и не идеально, так как, как я уже сказал, mainMethod вызывается много раз, и я не хочу никаких условий гонки при установке переменных результата. В идеале я хотел бы сделать их локальными переменными, но я не могу сделать их доступными из метода run, если они не являются окончательными, и тогда я не могу присвоить им значения...

Еще один подход, о котором я подумал, заключался в следующем:

public void mainMethod() throws InterruptedException, ExecutionException {
    String obj1, obj2;

    final ExecutorService executorService = Executors.newFixedThreadPool(16);
    final Future<String> res1 = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return expensiveMethod("param1");
        }
    });
    final Future<String> res2 = executorService.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return expensiveMethod("param2");
        }
    });

    obj1 = res1.get();
    obj2 = res2.get();

}

private String expensiveMethod(String param) {
    // Do work and return result
}

Это автоматически ожидает этих двух вычислений из метода main и позволяет мне сохранять результаты локально. Что вы, ребята, думаете? Любые другие подходы?

Ответ 1

Ваш подход с ExecutorService - это самый современный и безопасный способ сделать это. Рекомендуется выделить ваш Callable для разделения класса:

public class ExpensiveTask implements Callable<String> {

    private final String param;

    public ExpensiveTask(String param) {
        this.param = param;
    }

    @Override
    public String call() throws Exception {
        return expensiveMethod(param);
    }

}

что сделает ваш код намного чище:

final ExecutorService executorService = Executors.newFixedThreadPool(16);
final Future<String> res1 = executorService.submit(new ExpensiveTask("param1"));
final Future<String> res2 = executorService.submit(new ExpensiveTask("param2"));
String obj1 = res1.get();
String obj2 = res2.get();

Несколько примечаний:

  • 16 потоков слишком много, если вы хотите обрабатывать одновременно две задачи - или, возможно, вы хотите повторно использовать этот пул из нескольких потоков клиентов?

  • не забудьте закрыть пул

  • используйте легкий ExecutorCompletionService, чтобы дождаться завершения первой задачи, не обязательно для первой, которая была отправлена.

Если вам нужна совершенно другая дизайнерская идея, посмотрите с его моделью на основе concurrency.

Ответ 2

Вы можете пойти с ExecutorService

ExecutorService pool = Executors.newFixedThreadPool(4);

List<Future<String>> futures = new ArrayList<Future<String>>(10);

for(int i = 0; i < 10; i++){
   futures.add(pool.submit(new StringTask()));
}

for(Future<String> future : futures){
   String result = future.get();

   //Compute the result
}

pool.shutdown();

Или с CompletionService как:

ExecutorService threadPool = Executors.newFixedThreadPool(4);
CompletionService<String> pool = new ExecutorCompletionService<String>(threadPool);

for(int i = 0; i < 10; i++){
   pool.submit(new StringTask());
}

for(int i = 0; i < 10; i++){
   String result = pool.take().get();

   //Compute the result
}

threadPool.shutdown();

Отметьте этот для справки

Ответ 3

Во-первых, вы можете сделать экстернализацию создания ExecutorService из mainMethod(). Если это часто вызывается, вы потенциально создаете много потоков.

Future лучше, так как это именно то, для чего предназначены фьючерсы. Кроме того, он значительно упрощает чтение кода.

В более светлой ноте, хотя вам может потребоваться определить ваши объекты как окончательные, вы всегда можете иметь методы setter для объекта, который может быть вызван независимо от того, какая ваша ссылка является окончательной или нет, потенциально позволяя вам изменять значения конечных объектов, (Ссылки - это конечные объекты!)

Ответ 4

Вы хотите использовать CompletionService и отслеживать отправленные задачи.
В вашем цикле вы берете() и выходите из цикла, когда у вас все задачи завершены.
Масштабировать очень хорошо, вы добавляете больше задач позже.

Ответ 5

Я добавлю предложение, которое в моих глазах более элегантно, чем создание совершенно нового класса для вашего параметризованного Callable. Мое решение - это метод, возвращающий экземпляр Callable:

Callable<String> expensive(final String param) {
  return new Callable<String>() { public String call() { 
    return expensiveMethod(param);
  }};
}

Это даже делает клиентский код более привлекательным:

final Future<String> f1 = executor.submit(expensive("param1"));

Ответ 6

Совсем иной подход:

  • создать LinkedBlockingQueue

  • передать его каждой задаче. Задачами могут быть потоки или Runnables при j.u.c.Executor.

  • каждая задача добавляет свой результат в очередь

  • основной поток читает результаты, используя queue.take() в цикле

Таким образом, результаты обрабатываются, как только они вычисляются.

Ответ 7

private static final ExecutorService threadpool = Executors.newFixedThreadPool(3);

    ArrayList<Future<List<Data>>> futures = new ArrayList<Future<List<Data>>>();
    for (ArrayList<Data> data : map.values()) {
        final Future<List<Data>> future = threadpool.submit(new ValidationTaskExecutor(data));
        futures.add(future);
    }
    List<List<Data>> res = new ArrayList<List<Data>>();
    for (Future<List<Data>> future : futures) {
        allValidRequest.addAll(future.get());

    }