null

Использование FutureTask и Callable совместно с многопоточностью для повышения производительности Java-приложений

В качестве примера, рассмотрим простое REST-приложение на Spring Boot.

Суть приложения - обратиться к удаленному сервису за информацией о пользователе и возвратить полученные данные в формате JSON.

Код приложения

Приложение состоит из нескольких классов, основные из которых: UserController и UserService.

UserController.java

Данный класс является REST эндпоинтом. Класс содержит метод getUserInfo, который доступен пользователю по адресу "/getUserInfo".

Метод возвращает информацию о конкретном пользователе.

В метод необходимо передать параметр userId - ид пользователя:

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/getUserInfo")
    public String getUserInfo(@RequestParam("userId")long userId){
        return userService.getUserInfo(userId).toString();
    }
}

 

UserService.java

Класс UserService содержит бизнес-логику для получения необходимой информации о пользователе.

RemoteService в нём - это класс, олицетворяющий какой-либо удаленный сервис, например, стороннее АПИ, базу данных и т.д.

В коде мы обращаемся к удаленному сервису, чтобы получить имя (userName), баланс (userMoney) и страну проживания пользователя (userCountry).

Каждая характеристика запрашивается отдельно, однако в итоге мы объединяем всё воедино и возвращаем результирующий JSONObject.

Также мы фиксируем затрачиваемое время:

@Service
public class UserService {

    @Autowired
    private RemoteService remoteService;

    public String getUserInfo(long userId) {
        var currentTimeMillis = System.currentTimeMillis();

        //1. Call userName API
        JSONObject name = remoteService.getUserName(userId);
      
        //2. Call userMoney API
        JSONObject money = remoteService.getUserMoney(userId);
  
        //3. Call userCountry API
        JSONObject country = remoteService.getUserCountry(userId);
 
        //4. Combine API results altogether
        JSONObject result = new JSONObject();
        result.putAll(name);
        result.putAll(money);
        result.putAll(country);

        //5. Print consumed time
        System.out.println("Time consumed: " + (System.currentTimeMillis() - currentTimeMillis));

        //6.
        return result;
    }
}

 

Последовательное выполнение кода - медленный вариант по-умолчанию

Очевидно, что в классе UserService мы получаем информацию о пользователе по очереди - сначала userName, затем userMoney, затем userCountry.

Эндпоинт, развернутый на моей локальной машине, по адресу http://localhost:8080/GetUserInfo?userId=1, выдал следующий результат:

{"Amount": "$ 500", "Country": "Singapore", "Name":"DN Tech"}

На выполнение было затрачено 3065 мс - около 3-х секунд.

Давайте представим, что будет, если мы увеличим количество запрашиваемой из внешнего сервиса информации. Например, добавим получение адреса доставки, банковских реквизитов, номера телефона пользователя... Список можно продолжать бесконечно. Очевидно, всё это приведет к сильному увеличению затрачиваемого времени.

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

Да, существует, и заключается он в применении многопоточности.

Многопоточность с использованием FutureTask и Callable

Многопоточность - не новая тема.

Каждый разработчик Java знает, что, в мире многопоточности этого языка, существует класс Thread, который очень просто использовать:

Thread t = new Thread(new Runnable() {
         @Override
         public void run() {
         }
   })

Однако из-за Runnable мы не можем ничего вернуть в методе run().

В случае, когда мы хотим вернуть какие-то объекты, нам нужно поступить по-другому - использовать Callable. 

Callable принимает тип, который необходимо вернуть (в нашем случае - JSONObject).

Подобно методу run() в Runnable, Callable тоже имеет свой метод, но называется он по другому, - call().

Создадим вызываемый объект Callable для получения имени пользователя:

var userNameCallable = new Callable<JSONObject>() {
    @Override
    public JSONObject call() throws Exception {
        //1. Call userName API
        return remoteService.getUserName(userId);
    }
};

Аналогично, для userMoney и userCountry:

var userMoneyCallable = new Callable<JSONObject>() {
    @Override
    public JSONObject call() throws Exception {
        //2. Call userMoney API
        return remoteService.getUserMoney(userId);
    }
};

var userCountryCallable = new Callable<JSONObject>() {
    @Override
    public JSONObject call() throws Exception {
        //3. Call userCountry API
        return remoteService.getUserCountry(userId);
    }
};

Теперь, чтобы запустить все три процесса одновременно, нам нужно использовать класс Thread, примерно так:

new Thread(userNameCallable).start();

Однако мы не можем сделать это напрямую, так как параметр конструктора Thread должен реализовать интерфейс Runnable (как вы уже поняли, Callable его не реализует).

Чтобы решить эту проблему, можно прибегнуть к FutureTask.

FutureTask - это класс, реализующий интерфейс RunnableFuture, который в свою очередь расширяет интерфейс Runnable.

Создадим три FutureTask, передав им экземпляры Callable, которые мы инстанцировали ранее:

FutureTask<JSONObject> nameFutureTask = new FutureTask<>(userNameCallable);
FutureTask<JSONObject> moneyFutureTask = new FutureTask<>(userMoneyCallable);
FutureTask<JSONObject> countryFutureTask = new FutureTask<>(userCountryCallable);

Далее передадим эти FutureTask экземплярам Thread для реализации многопоточности:

new Thread(nameFutureTask).start();
new Thread(moneyFutureTask).start();
new Thread(countryFutureTask).start();

Теперь нам остается только объединить все результаты от каждого объекта Callable в один JSONObject:

//4. Combine API results altogether
JSONObject result = new JSONObject();
try {
    result.putAll(nameFutureTask.get());
    result.putAll(moneyFutureTask.get());
    result.putAll(countryFutureTask.get());
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
}

 

После улучшений, выполнение кода заняло 1049 мс - всего одну секунду! И это без ущерба для результата. Даже если мы добавим много дополнительной информацию о пользователе, временные затраты по прежнему будут составлять около 1-ой секунды.

Очевидно, что нам удалось добиться улучшения производительности приложения. Однако, это не единственный способ как это можно сделать. У нас есть возможность получить схожую производительность с помощью parallelStream, и при этом реализация будет намного лаконичнее!

Мы поговорим об этом в одной из следующих статей. Оставайтесь с нами :)

p.s.

Статья основана на труде "Use FutureTask and Callable with Multithreading to Boost Your Java Application Performance" автора DN Tech.