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 вызывается много раз, и я не хочу никаких условий гонки при установке переменных результата. В идеале я хотел бы сделать их локальными переменными, но я не могу сделать их доступными из метода запуска, если только они не являются окончательными, и тогда я не могу присвоить им значения...

Другой подход, о котором я думал, заключался в следующем:

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
}

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


person Bober02    schedule 11.10.2012    source источник
comment
Использование Future — разумная идея.   -  person Kalpak Gadre    schedule 11.10.2012


Ответы (6)


Ваш подход с 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 для ожидания первая завершенная задача, не обязательно первая отправленная.

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

person Tomasz Nurkiewicz    schedule 11.10.2012
comment
Можно ли использовать этот подход одновременно? - person Code Junkie; 04.11.2013

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

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

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

person Kalpak Gadre    schedule 11.10.2012

Немного другой подход:

  • создать LinkedBlockingQueue

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

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

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

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

person Alexei Kaigorodov    schedule 11.10.2012
comment
Это хорошая идея, если число обрабатываемых параметров неограничено. У OP, кажется, есть только пара из них, но ему нужно часто повторно запускать эти парные вычисления. - person Marko Topolnik; 11.10.2012
comment
@Алексей, можешь поделиться примером подхода? - person Panchaxari Hiremath; 10.12.2019

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

person Johnny Hujol    schedule 12.10.2012

Я добавлю предложение, которое, на мой взгляд, более элегантно, чем создание целого нового класса для вашего параметризованного 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"));
person Marko Topolnik    schedule 11.10.2012

person    schedule
comment
Добавьте также некоторые пояснения. - person ketan; 01.02.2016