Простейший, путь – явно создать два объекта типа Thread, передать им инстансы Runnable, с нужными задачами в реализации их методов run, и запустить вызвав thread.start(). Если в основном потоке нужно дождаться завершения задач – после start() вызывается метод thread.join(). Исполнение зависнет на вызове этого метода до тех пор, пока тред не закончит свою задачу и не умрет. Вся работа задач с внешними данными должна быть синхронизирована.

Такое ручное создание тредов полезно в учебных целях, но считается плохой практикой в промышленном коде: само создание – дорогостоящая операция, а большое количество случайно созданных потоков может приводить к проблеме голодания (starvation) потоков.

В качестве продвинутой альтернативы используются пуллы потоков – реализации интерфейса ExecutorService. Такие сервисы создаются статическими фабричными методами класса Executors. Они умеют принимать задачи в виде Runnable- или Callable-объектов на заранее созданном наборе потоков (собственно, пулле).

Кроме самого пулла, экземпляры ExecutorService содержат фабрику потоков («инструкцию» как создать тред при необходимости), и коллекцию-очередь задач на исполнение.

В ответ на передачу на исполнение Runnable или Callable, сервис возвращает связанный с ним объект типа Future – хранилище, которое будет заполнено результатом выполнения задачи в будущем. Даже если никакого результата не ожидается, Future поможет дождаться момента завершения обработки задачи.

В Android для асинхронного выполнения используется похожая сущность – Looper.