fundamentals/os/notes/03-threads-synchronisation.md
2022-10-15 16:22:27 +01:00

4.8 KiB

Threads and Synchronisation

Executor

The Executor interface is used to execute tasks. It is a generic interface that can be used to execute any kind of task. The Executor interface has only one method:

public interface Executor {
    void execute(Runnable command);
}

The execute method takes a Runnable object as a parameter. The Runnable interface is a functional interface that has only one method. Executors internally use a thread pool to execute the tasks. The execute method is non-blocking. It returns immediately after submitting the task to the thread pool. The execute method is used to execute tasks that do not return a result.

A thread pool is a collection of threads that are used to execute tasks. Instead of creating a new thread for each task, a thread pool reuses the existing threads to execute the tasks. This improves the performance of the application.

The Executor interface has a method called newCachedThreadPool that returns an ExecutorService object. The ExecutorService interface extends the Executor interface. The ExecutorService interface has methods to execute tasks that return a result. The ExecutorService interface also has methods to shutdown the thread pool.

To run a task using the Executor interface, we can use the newCachedThreadPool method to create an ExecutorService object. The newCachedThreadPool method returns an ExecutorService object that uses a thread pool with a variable number of threads. The newCachedThreadPool method creates a new thread for each task if there are no idle threads in the thread pool. If there is an idle thread in the thread pool, the newCachedThreadPool method reuses the idle thread to execute the task. The newCachedThreadPool method returns an ExecutorService object that uses a thread pool with a variable number of threads.

Executor executorService = Executors.newCachedThreadPool();
executorService.execute(() -> System.out.println("Hello World"));

Callable and Future

Runnables do not return a result. If we want to execute a task that returns a result, we can use the Callable interface. The Callable interface is a functional interface that has only one method:

public interface Callable<V> {
    V call() throws Exception;
}

The call method returns a result of type V. The call method can throw an exception. The Callable interface is used to execute tasks that return a result. For instance we can use the Callable interface to execute a task that returns the sum of two numbers:

Callable<Integer> sumTask = () -> 2 + 3;

In order to execute a task that returns a result, we can use the submit method of the ExecutorService interface. The submit method takes a Callable object as a parameter. The submit method returns a Future object. The Future interface has a method called get that returns the result of the task. The get method is a blocking method. It waits until the task is completed and then returns the result of the task.

ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future = executorService.submit(() -> 2 + 3);
Integer result = future.get();

Futures can be used to cancel tasks. The Future interface has a method called cancel that can be used to cancel a task. The cancel method takes a boolean parameter. If the boolean parameter is true, the task is cancelled even if the task is already running. If the boolean parameter is false, the task is cancelled only if the task is not running.

ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future = executorService.submit(() -> 2 + 3);
future.cancel(false);

Synchronisation

Whenever we have multiple threads that access the same resource, we need to make sure that the threads do not interfere with each other. This is called synchronisation.

Synchronisation can be seen in the adder and subtractor example. The adder and subtractor threads access the same counter variable. If the adder and subtractor threads do not synchronise, the counter variable can be in an inconsistent state.

  • Create a count class that has a count variable.
  • Create two different classes Adder and Subtractor.
  • Accept a count object in the constructor of both the classes.
  • In Adder, iterate from 1 to 100 and increment the count variable by 1 on each iteration.
  • In Subtractor, iterate from 1 to 100 and decrement the count variable by 1 on each iteration.
  • Print the final value of the count variable.
  • What would the ideal value of the count variable be?
  • What is the actual value of the count variable?
  • Try to add some delay in the Adder and Subtractor classes using inspiration from the code below. What is the value of the count variable now?