Статьи

java.util.concurrent. Основы будущего

Настоящим я начинаю серию статей о концепции будущего на языках программирования (также называемых обещаниями или задержками ) под рабочим названием: « Назад в будущее» . Фьючерсы являются очень важной абстракцией, даже больше в наши дни, чем когда-либо, из-за растущего спроса на асинхронные, управляемые событиями, параллельные и масштабируемые системы. В первой статье мы познакомимся с наиболее простым интерфейсом java.util.concurrent.Future<T> . Позже мы перейдем к другим фреймворкам, библиотекам или даже языкам. Future<T> довольно ограничено, но важно понимать, эхм , будущие части.

В однопоточном приложении, когда вы вызываете метод, оно возвращается только после завершения вычислений ( IOUtils.toString() происходит из Apache Commons IO ):

1
2
3
4
5
6
7
8
9
public String downloadContents(URL url) throws IOException {
    try(InputStream input = url.openStream()) {
        return IOUtils.toString(input, StandardCharsets.UTF_8);
    }
}
  
//...
  
final String contents = downloadContents(new URL("http://www.example.com"));

downloadContents() выглядит безобидно 1 , но это может занять даже много времени. Более того, чтобы уменьшить задержку, вы можете выполнять другую независимую обработку в ожидании результатов. В старые времена вы запускали новый Thread и как-то ждали результатов (общая память, блокировки, ужасная пара wait() / notify() и т. Д.). С Future<T> это намного приятнее:

1
2
3
4
5
6
7
public static Future<String> startDownloading(URL url) {
    //...
}
  
final Future<String> contentsFuture = startDownloading(new URL("http://www.example.com"));
//other computation
final String contents = contentsFuture.get();

Мы скоро startDownloading() . На данный момент важно, чтобы вы понимали принципы. startDownloading() не блокируется, ожидая внешнего сайта. Вместо этого он немедленно возвращается, возвращая легкий объект Future<String> . Этот объект обещает, что String будет доступен в будущем. Не знаю когда, но сохраните эту ссылку, и как только она Future.get() , вы сможете получить ее с помощью Future.get() . Другими словами, Future — это прокси или оболочка вокруг объекта, которого еще нет. Как только асинхронное вычисление выполнено, вы можете извлечь его. Итак, какой API предоставляет Future ?

Future.get() — самый важный метод. Он блокирует и ждет, пока обещанный результат не будет доступен ( решен ). Так что если нам действительно нужна эта String , просто вызовите get() и подождите. Существует перегруженная версия, которая принимает тайм-аут, поэтому вы не будете ждать вечно, если что-то пойдет не так. TimeoutException если ждет слишком долго.

В некоторых случаях вы можете заглянуть в Future и продолжить, если результат еще не доступен. Это возможно с помощью isDone() . Представьте себе ситуацию, когда ваш пользователь ожидает каких-то асинхронных вычислений, и вы хотите, чтобы он знал, что мы все еще ждем, и в то же время проведем некоторые вычисления:

1
2
3
4
5
final Future<String> contentsFuture = startDownloading(new URL("http://www.example.com"));
while (!contentsFuture.isDone()) {
    askUserToWait();
    doSomeComputationInTheMeantime();
}

Последний вызов Future.isDone() гарантированно немедленно возвращается, а не блокируется, потому что Future.isDone() вернул true . Если вы следуете шаблону выше, убедитесь, что вы не заняты ожиданием, вызывая isDone() миллионы раз в секунду.

Отмена фьючерса — последний аспект, который мы еще не рассмотрели. Представьте, что вы начали какую-то асинхронную работу, и вы можете только ждать ее в течение определенного времени. Если этого не произойдет, скажем, через 2 секунды, мы сдаемся и либо распространяем ошибку, либо обходим ее. Однако, если вы хороший гражданин, вы должны как-то сказать этому будущему объекту: вы мне больше не нужны, забудьте об этом. Вы экономите ресурсы обработки, не выполняя устаревшие задачи. Синтаксис прост:

1
contentsFuture.cancel(true);    //meh...

Мы все любим загадочные, логические параметры, не так ли? Отмена приходит в двух вариантах. mayInterruptIfRunning значение mayInterruptIfRunning параметру mayInterruptIfRunning мы отменяем только те задачи, которые еще не были запущены, когда Future представляет результаты вычислений, которые даже не начались. Но если наш Callable.call() уже находится в середине, мы даем ему закончить. Однако, если мы передадим true , Future.cancel() будет более агрессивным, пытаясь прервать уже запущенные задания. Как? Подумайте обо всех этих методах, которые Thread.sleep() печально известную InterruptedException , а именно Thread.sleep() , Object.wait() , Condition.await() и многих других (включая Future.get() ). Если вы блокируете какой-либо из этих методов, и кто-то решил отменить ваш Callable , он фактически сгенерирует InterruptedException , сигнализируя, что кто-то пытается прервать текущее выполнение задачи.

Итак, теперь мы понимаем, что такое Future<T> — заполнитель чего-то, что вы получите в будущем. Это как ключи от машины, которая еще не была изготовлена. Но как вы на самом деле получаете экземпляр Future<T> в своем приложении? Два наиболее распространенных источника — это пулы потоков и асинхронные методы (для вас они поддерживаются пулами потоков) Таким образом, наш startDownloading() можно переписать так:

01
02
03
04
05
06
07
08
09
10
11
12
private final ExecutorService pool = Executors.newFixedThreadPool(10);
  
public Future<String> startDownloading(final URL url) throws IOException {
    return pool.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            try (InputStream input = url.openStream()) {
                return IOUtils.toString(input, StandardCharsets.UTF_8);
            }
        }
    });
}

Много синтаксического шаблона, но основная идея проста: обернуть длительные вычисления в Callable<String> и submit() их в пул потоков из 10 потоков. Отправка возвращает некоторую реализацию Future<String> , скорее всего, каким-то образом связанную с вашей задачей и пулом потоков. Очевидно, ваша задача не выполняется сразу. Вместо этого он помещается в очередь, которая позже (может быть, даже намного позже) опрашивается потоком из пула. Теперь должно быть ясно, что означают эти два вида cancel() — вы всегда можете отменить задачу, которая все еще находится в этой очереди. Но отменить уже запущенную задачу немного сложнее.

Еще одно место, где вы можете встретить Future — это Spring и EJB. Например, в @Async Spring вы можете просто аннотировать свой метод с помощью @Async :

1
2
3
4
5
6
7
8
@Async
public Future<String> startDownloading(final URL url) throws IOException {
    try (InputStream input = url.openStream()) {
        return new AsyncResult<>(
                IOUtils.toString(input, StandardCharsets.UTF_8)
        );
    }
}

Обратите внимание, что мы просто AsyncResult наш результат в AsyncResult реализуя Future . Но сам метод не имеет дело с пулом потоков или асинхронной обработкой. Позже Spring проксирует все вызовы startDownloading() и запускает их в пуле потоков. Точно такая же возможность доступна через аннотацию @Asynchronous в EJB .

Итак, мы многое узнали о java.util.concurrent.Future . Теперь пришло время признать — этот интерфейс довольно ограничен, особенно по сравнению с другими языками. Подробнее об этом позже.

1 — вы не знакомы с функцией пробного использования ресурсов в Java 7? Вам лучше перейти на Java 7 сейчас . Java 6 больше не будет поддерживаться через две недели .

Ссылка: java.util.concurrent.Future основы от нашего партнера JCG Томаша Нуркевича в блоге NoBlogDefFound .