Multithreading

7 minute read

Runnable vs Callable

Runnable doesn’t return anything (void) nor does it explicitly indicate a check exception.

Whereas Callable returns an object of type T and throws exception

Daemon Threads:

A daemon thread is a thread that runs in the background and does not prevent the JVM from exiting.

  • When all non-daemon threads finish, the JVM can shut down, even if daemon threads are still running.
  • Daemon threads are typically used for background services like garbage collection or background monitoring.

Defining Platform Threads

In Java, by default, all threads are non-daemon threads (unless explicitly modified)

  • The Java Virtual Machine (JVM will not terminate until all non-daemon threads have finished executing.
  • This is true even if the main thread has terminated.
thread.setDaemon(false);//false=non-daemon, runs the child thread, even if the parent dies. 
// if set true, the child dies as soon as parent dies

T1ThreadRunsParentDies.java

extending Thread

Invoke

// start a thread from a Thread
Thread thread = new SimpleThread();
thread.start();

Define

class SimpleThread extends Thread {
    @Override
    public void run() {
    }
}

Implementing Runnable

invoke the thread

Runnable runnable = new SimpleRunnable();
Thread thread = new Thread(runnable);

Definition

class SimpleRunnable implements Runnable {
    @Override
    public void run() {
    }
}
Difference between t.start() and t.run()
  • t.start calls run() from within. if t.run is executed, run method will execute normally.

  • ALSO. Since by extending, we are limiting to extending only one class. NO CHANCE OF EXTENDING ANY OTHER CLASS!!
  • We cannot extend any other class. Thus implementing Runnable Interface is preferred over this approach.

Using Fluent API

Staring Daemon thread

// start a daemon thread using Fluent API
Runnable r = new SimpleRunnable();
Thread thread = Thread.ofPlatform().name("Simple").daemon(true).start(r);

Using Lambda

No need to explicitly use thread.join() as the Labmda (from the caller Thread) would get executed priort to moving onto the next line

Thread.ofPlatform().start(() -> {
        TimeUnit.SECONDS.sleep(5);
});

Using method Reference

Thread thr = new Thread(MethodReferenceClass::doSomething);

Thread.ofPlatform().start(MethodReferenceClass::doSomething);

Thread methods

Thread.currentThread();
thread.interrupt();
boolean isInterrupted= thread.isInterrupted();

thread.join();

thread.sleep(Duration.ofSeconds(2));

thread.setDaemon(true);

Unfortunately, the recommended approach (if you’re using any of the application servers like Tomcat and WebLogic) is none of the above. Creating platform threads is highly discouraged.

BREATHER

Platform threads - Issues

Platform Thread is an expensive resource. Each thread is allocated 1 MB of memory by default.

Another problem is that starting a platform thread will take some time which might lead to performance issues.

The solution for both the problems is to create a thread pool (every application server creates a default thread-pool).

The size of the thread pool that is used in Spring Boot with Tomcat

  • by default, Tomcat uses a thread-pool size of 200.
  • It means that If 250 concurrent users hit spring boot application,
    • 50 of them are going to wait for a platform thread to process their request
  • a user request for an application server would be handed over to an already created thread in a thread pool, rather than creating a brand new one.

Fundamental shift in the thinking

Instead of creating a new thread to do a particular task, think about submitting a task to a thread pool

Thinking in this way allows us to separate the task from how the task will be executed.

Telling what to do not HOW to do

We call this the execution policy of the task, which is the idea behind the Java Futures. The thread pool contains a number of threads based on some policy

Executor Service & Futures

  • Several callers can submit their tasks.
  • These tasks get executed by the threads inside the thread pool.
  • But note that after submission, the caller gets what is called as the future **, which is a **logical reference to the executing task.
  • The task submission itself does not wait, but simply queues the request to the thread pool and returns back immediately with the future.

Now, each one of these concepts require a representation in Java.

In terms of interfaces for Tasks

  • Runnable and
  • Callable Any Java class which implements one of these Java interfaces is considered a task(could be a Lambda as both are functional interfaces)

interface which represents the abstraction of a thread pool is

  • Executor Service

The applications will use the executor service interface to submit runnable or callable tasks.

Executor Service

Returns a Future

public interface ExecutorService extends Executor, AutoCloseable{
    Future<?> submit(Runnable task);//Submits a Runnable task for execution and returns a Future representing that task.
    default void close();//Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
    void shutdown();//Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
    List<Runnable> shutdownNow();//Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.
    .....
}

Note here that the executor service interface also implements AutoCloseable

  • if you wrap executor service in a try with resources block, then the close() method will be automatically called.
  • good use in virtual threads.

A thread executor creates a non-daemon thread on the first task taht is executed, so failing to call

  • shutdown() will result in your application never terminating
  • shutdownNow() attempts to stop all running thread

futures.png

Create ExecutorServices

Keep in mind to either use try-with-resource block or shout down the service in the finally block

Fixed Thread Pool
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads);
Cached Thread Pool:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
Single Thread Executor:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
Scheduled Thread Pool
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

Submitting a Task

Runnable Service
ExecutorService executor = Executors.newFixedThreadPool(2);

Runnable task1 = () -> {
    System.out.println("Task 1 is running");
};

executor.execute(task2);
Callable Service (Retruns a Future)
Callable<String> task1 = () -> {
    return "Task 1 result";
};

Future<String> future1 = executor.submit(task1);

try {
    String result1 = future1.get(); // Blocking call
    System.out.println(result1);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

Shutting Down the ExecutorService

executor.shutdown(); // Initiates an orderly shutdown
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
        executor.shutdownNow(); // Forcefully shuts down if tasks do not terminate within the timeout
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

ExecutorCompletionService

to handle the results of the tasks as they happen instead of get()

Futures Limitations

  • Cannot create Asynchronous pipeline (Reactive style of programming) (its imperative style. Have to tell what to do and how to do)
  • Cannot Complete a future
  • Limited Functionality

CompletableFutures

Refer CompletableFutures

// Create a Completable Future
CompletableFuture<String> future = new CompletableFuture<>();

Problems with CompletableFutures

Railway track pattern is good in concept, but in implementation,

  • cognitive load : since there is skipping to then’s or exceptionally’s or the return type.

Continuations and Coroutines

  • Don’t want a Thread be blocked on a task. thread should be able to switch between the tasks
  • when a task is sleeping, thread should be able to do other things

Subroutine : Just a function, no state. Function you call and get a response.

Coroutine : Cooperative Routine - no entry point, no exit point. Just like a conversations. Kind of weave in and weave out of the functions.

Continuations : Data structure that helps to restore the context of a call between calls to a coroutine

  • Should be a data structure that you benefit from but should not be directly accessed. Be in the background.
  • Continuations in Java are behind the scenes.

when a method yields using Continuation.yield() method, the Stack Frames and Code Pointer get stored in the related Continuation.

  • A Continuation can be run from inside another Continuation

Virtual threads occupy very small amount of memory and uses the concept of mounting and unmounting in terms of context switching when Sleep or blocking operation is involved.

Thread can switch between tasks

//Do not confuse ExecutorService with pooling vc No sense to pool virtual threads

VirtualThreads are like qtips - use and throw

DONE in : Main thread Thread[#1,main,5,main]
entering task1 Thread[#1,main,5,main]
entering task2 Thread[#1,main,5,main] #when the task 1 goes to sleep, same thread picks up task2
exiting task2 Thread[#1,main,5,main]
exiting task1 Thread[#1,main,5,main] - Value of x = 90 #When the coroutine comes back to task1, it remembers the 
state and prints the value. This is done via continuations