Executing Dependent and Independent tasks in parallel using Java

Thameem Ansari
Javarevisited
Published in
4 min readJun 28, 2023

--

Photo by John Anvik on Unsplash

Introduction

In this article I am going to show you how to call dependent and independent tasks in parallel using Java CompletableFuture. So basically three tasks will be invoked parallelly and in parallel execution the response time will be very less. I am going to show you how to execute concurrently using Java 8 with CompletableFuture.

Problem Statement

Lets say Task A, Task B and Task C to execute as dependent and independent tasks in parallel mode with below conditions.

  1. Task A and Task C can run independently.
  2. Task B is dependent on the output of Task A.

In order to achieve we will make use of a simple Barrista making coffee scenario.

Use Case

Lets take a barrista making coffee. The tasks to do are:

  1. Grind the required coffee beans (no preceding tasks)
  2. Heat some water (no preceding tasks)
  3. Brew an espresso using the ground coffee and the heated water (depends on 1 & 2)
  4. Froth some milk (no preceding tasks)
  5. Combine the froth milk and the espresso (depends on 3,4)
Barrista Scenario

Code Sample

package com.demo.myexamples.service;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiFunction;
import java.util.function.Supplier;

public class Barrista
{

// number of threads used in executor
static final int NOTHREADS = 3;

// time of each task
static final int HEATWATER = 1000;
static final int GRINDBEANS = 1000;
static final int FROTHMILK = 1000;
static final int BREWING = 1000;
static final int COMBINE = 1000;

// method to simulate work (pause current thread without throwing checked exception)
public static void pause(long t)
{
try
{
Thread.sleep(t);
}
catch(Exception e)
{
throw new Error(e.toString());
}
}

// task to heat some water
static class HeatWater implements Supplier<String>
{
@Override
public String get()
{
System.out.println("Heating Water");
pause(HEATWATER);
return "hot water";
}
}

// task to grind some beans
static class GrindBeans implements Supplier<String>
{
@Override
public String get()
{
System.out.println("Grinding Beans");
pause(GRINDBEANS);
return "grinded beans";
}
}

// task to froth some milk
static class FrothMilk implements Supplier<String>
{
@Override
public String get()
{
System.out.println("Frothing some milk");
pause(FROTHMILK);
return "some milk";
}
}

// task to brew some coffee
static class Brew implements BiFunction<String,String, String>
{
@Override
public String apply(String groundBeans, String heatedWater)
{
System.out.println("Brewing coffee with " + groundBeans + " and " + heatedWater);
pause(BREWING);
return "brewed coffee";
}
}

// task to combine brewed coffee and milk
static class Combine implements BiFunction<String,String, String>
{
@Override
public String apply(String frothedMilk, String brewedCoffee)
{
System.out.println("Combining " + frothedMilk + " "+ brewedCoffee);
pause(COMBINE);
return "Final Coffee";
}
}

public static void main(String[] args)
{
ExecutorService executor = Executors.newFixedThreadPool(NOTHREADS);

long startTime = System.currentTimeMillis();

try
{
// create all the tasks and let the executor handle the execution order
CompletableFuture<String> frothMilk = CompletableFuture.supplyAsync(new FrothMilk(), executor);
CompletableFuture<String> heatWaterFuture = CompletableFuture.supplyAsync(new HeatWater(), executor);
CompletableFuture<String> grindBeans = CompletableFuture.supplyAsync(new GrindBeans(), executor);

CompletableFuture<String> brew = heatWaterFuture.thenCombine(grindBeans, new Brew());
CompletableFuture<String> coffee = brew.thenCombine(frothMilk, new Combine());

// final coffee
System.out.println("Here is the coffee:" + coffee.get());

// analyzing times:
System.out.println("\n\n");
System.out.println("Time taken using multi-threaded:\t\t" + (System.currentTimeMillis() - startTime)/1000.0);

// compute the longest possible time:
long longestTime = HEATWATER + GRINDBEANS + FROTHMILK + BREWING + COMBINE;
System.out.println("Time taken using single-threaded thread:\t" + longestTime/1000.0);
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
executor.shutdown();
}

}
}

Code output

Frothing some milk
Heating Water
Grinding Beans
Brewing coffee with hot water and grinded beans
Combining brewed coffee some milk
Here is the coffee:Final Coffee


Time taken using multi-threaded: 3.027
Time taken using single-threaded thread: 5.0

In our case, Total time taken by all five tasks called in single-threaded mode is 1000 + 1000 + 1000 + 1000 + 1000 = 5000ms. In reality if you execute and wait for them to complete, then you have to incur the cost of waiting for the task that takes longest time. However, when we execute the same in mutli-threaded mode the total time taken is 3027ms.

Conclusion

In real use cases it makes sense to make multiple calls concurrently or rather it sometimes becomes an unavoidable situation where you need to make some calls to multiple services at the same time. Making concurrent calls are good ideas because it will reduce the time required to complete the whole operation rather than spending the sum of time spent over the span of all calls.

--

--

Thameem Ansari
Javarevisited

Technology Expert| Coder| Sharing Experience| Spring | Java | Docker | K8s| DevOps| https://reachansari.com