Why we need CustomThreadPoolExecutor when we can use ExecutorService Framework to create and manage Threads?

ExecutorService executor = Executors.newFixedThreadPool(20);

is nothing but

return new ThreadPoolExecutor(20, 20,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());

under the hoods.

ThreadPoolExecutor would be more effective if you have customized many or all of below parameters.

ThreadPoolExecutor(int corePoolSize, 
               int maximumPoolSize, 
               long keepAliveTime, 
               TimeUnit unit, 
               BlockingQueue<Runnable> workQueue, 
               ThreadFactory threadFactory,
               RejectedExecutionHandler handler)

How it works?

  1. If fewer than corePoolSize threads are running, try to start a new thread with the given command as its first task. The call to addWorker atomically checks runState and workerCount, and so prevents false alarms that would add threads when it shouldn’t, by returning false.
  2. If a task can be successfully queued, then we still need to double-check whether we should have added a thread (because existing ones died since last checking) or that the pool shut down since entry into this method. So we recheck state and if necessary roll back the enqueuing if stopped, or start a new thread if there are none.
  3. If we cannot queue task, then we try to add a new thread. If it fails, we know we are shut down or saturated and so reject the task.

CustomThreadPoolExecutor.java

import java.util.concurrent.*;

public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
    public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                    TimeUnit unit, BlockingQueue<Runnable> workQueue,
                                    ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }
}

CustomThreadFactory.java

import java.util.concurrent.ThreadFactory;

public class CustomThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable r) {
        Thread th = new Thread(r);
        th.setPriority(Thread.NORM_PRIORITY);
        th.setDaemon(false);
        return th;
    }
}

CustomRejectHandler.java

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomRejectHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("Task Rejected: "+ r.toString());
    }
}

Task.java

import java.util.concurrent.ThreadPoolExecutor;

public class Task implements Runnable{

    String taskName;
    ThreadPoolExecutor executor;
    Long timeInMilliSeconds;


    public Task(String taskName, ThreadPoolExecutor executor, Long timeInMilliSeconds) {
        this.taskName = taskName;
        this.executor = executor;
        this.timeInMilliSeconds = timeInMilliSeconds;
    }

    @Override
    public void run() {
        try{
            Thread.sleep(this.timeInMilliSeconds);

            System.out.print("Tasks in Blocking Queue "+ this.executor.getQueue().stream().toList() + ", ");
            System.out.print(this.taskName + " completed by " +  Thread.currentThread().getName() + " after running "+ timeInMilliSeconds +"ms" );
            System.out.println(", Active Threads available "+ executor.getPoolSize());
        }catch (Exception e){
        }
    }

    @Override
    public String toString() {
        return "Task{" +
                "taskName='" + taskName + '\'' +
                '}';
    }
}

BatchProcessor.java

public class BatchProcessor {
    public static void main(String[] args) throws InterruptedException {

        ThreadPoolExecutor executor = new CustomThreadPoolExecutor(2,4, 10, TimeUnit.MINUTES,
                                                                    new ArrayBlockingQueue<>(2),
                                                                    new CustomThreadFactory(),
                                                                    new CustomRejectHandler());

        System.out.println("Active Threads  available for processing at start "+  executor.getPoolSize());

        executor.submit( new Task("task1", executor, 2500L)); //Directly dealt by CorePool Thread
        executor.submit( new Task("task2", executor, 500L)); //Directly dealt by CorePool Thread
        Thread.sleep(2000L);
        System.out.println("Slept for 2000 Millisecond");
        executor.submit( new Task("task3", executor, 200L)); //Directly dealt by CorePool Thread
        executor.submit( new Task("task4", executor, 1000L)); //Directly dealt by CorePool Thread
        executor.submit( new Task("task5", executor, 300L)); //Dealt by extra thread within Maximum Pool Size
        executor.submit( new Task("task6",executor, 300L)); //Directly dealt by CorePool Thread

        executor.shutdown();
    }
}

Output

Active Threads  available for processing at start 0
Tasks in Blocking Queue [], task2 completed by Thread-1 after running 500ms, Active Threads available 2
Slept for 2000 Millisecond
Tasks in Blocking Queue Task{'task4','task6'}, task3 completed by Thread-1 after running 200ms, Active Threads available 3
Tasks in Blocking Queue Task{'task6'}, task5 completed by Thread-2 after running 300ms, Active Threads available 3
Tasks in Blocking Queue [], task1 completed by Thread-0 after running 2500ms, Active Threads available 3
Tasks in Blocking Queue [], task6 completed by Thread-2 after running 300ms, Active Threads available 2
Tasks in Blocking Queue [], task4 completed by Thread-1 after running 1000ms, Active Threads available 1

What is BlockingQueue?

  1. BlockingQueue is a Interface which has 4 Implementations – LinkedBlockingQueue, ArrayBlockingQueue, PriorityBlockingQueue, SynchronousQueue
  2. Thread Safe: BlockingQueue implementations are thread-safe, with all methods being atomic.
  3. Blocking Operation: Has blocking behavior if the queue is full (for producers) or empty (for consumers).
  4. No Null Elements: Attempts to insert a null will result in a NullPointerException.

Two Types of BlockingQueue

  • Bounded BlockingQueue: Fixed capacity, blocking producers when full.
  • Unbounded BlockingQueue: Expands as needed (e.g., backed by a LinkedList), though subject to memory constraints.

Simple Producer Consumer Implementation using BlockingQueue?

  1. We have a queBuffer which take max of 10 printing task at a time
  2. The printing Task are added from PrintProducer whereas it is polled at PrintConsumer end
  3. When you start the thread for producer you should use start() method rather than run() as run executes by taking control of main thread whereas start() spawns two new thread which makes producer and consumer run at same time in two different threads.

PrintProducer.java

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadLocalRandom;

public class PrintProducer extends Thread {
    private BlockingQueue queBuffer;

    public PrintProducer(BlockingQueue queBuffer) {
        this.queBuffer = queBuffer;
    }

    @Override
    public void run() {
        while(true){
            try {
                Integer randomNo = ThreadLocalRandom.current().nextInt(100);
                queBuffer.put(randomNo);
                System.out.println("Added Task No " + String.valueOf(randomNo));
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

PrintConsumer.java

import java.util.concurrent.BlockingQueue;

public class PrintConsumer extends Thread{
    private BlockingQueue queBuffer;

    public PrintConsumer(BlockingQueue queBuffer) {
        this.queBuffer = queBuffer;
    }

    @Override
    public void run() {
        while(true){
            try {
                System.out.println("Polled Task No " + queBuffer.take());
                Thread.sleep(1500);
            } catch (InterruptedException e) {

            }
        }
    }
}

ProcessPrints.java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProcessPrints {
    static BlockingQueue queBuffer = new ArrayBlockingQueue(10);

    public static void main(String[] args) {
        PrintProducer objPrintProducer = new PrintProducer(queBuffer);
        PrintConsumer objPrintConsumer = new PrintConsumer(queBuffer);

        objPrintProducer.start();
        objPrintConsumer.start();
    }
}

Output

Polled Task No 61
Added Task No 61
Added Task No 33
Added Task No 0
Polled Task No 33
Added Task No 29
Added Task No 93
Added Task No 20
Polled Task No 0
Added Task No 24
Added Task No 2
Added Task No 31
.
.
.
.

The above code can be implemented as below as Thread takes Runnable as argument with run() method definition in lambda expression

ProcessPrints.java

public class ProcessPrints {
    static BlockingQueue queBuffer = new ArrayBlockingQueue(10);

    public static void main(String[] args) {
        //Producer Implementation
        new Thread(()->{
            while(true){
                try {
                    Integer randomNo = ThreadLocalRandom.current().nextInt(100);
                    queBuffer.put(randomNo);
                    System.out.println("Added Task No " + String.valueOf(randomNo));
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

        //Consumer Implementation
        new Thread(()->{
            while(true){
                try {
                    System.out.println("Polled Task No " + queBuffer.poll());
                    Thread.sleep(1500);
                } catch (InterruptedException e) {

                }
            }
        }).start();
    }
}

Simple Program to print numbers using threads
NumberPrinter.java

public class NumberPrinter implements Runnable{
    int number;
    public NumberPrinter(int number){
        this.number = number;
    }

    public void run(){
        System.out.println("Printing Number from Thread "+ this.number);
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        for (int idx=1;idx<=5;idx++){
            Thread objthread = new Thread(new NumberPrinter(idx));
            objthread.start();
        }
    }
}

Output

Printing Number from Thread 5
Printing Number from Thread 1
Printing Number from Thread 4
Printing Number from Thread 3
Printing Number from Thread 2

Simple Program using Executor Service taking Runnable as Argument

ExecutorService is a framework which allows to create thread. Threads can be created from FixedThreadPool, CachedThreadPool and ScheduledThreadPool. submit() method takes runnable or callable object (Functional Interface Type) as argument. The Same code above can be rewritten as below
Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService objExecService = Executors.newFixedThreadPool(2);

        //Lambda Expresssion passed as Argument as Runnable is FI
        objExecService.submit(() -> {
            System.out.println(Thread.currentThread().getName());
        });

        objExecService.shutdown();
    }
}

Output

pool-1-thread-1

Same code with Runnable instance passed as argument to submit

  .
  . 
  //Instance of Runnable passed as argument
  HelloThread1 objHT1 = new HelloThread1();
  objExecService.submit(objHT1);
  .
  .

Output

Hello World from Thread Name (pool-1-thread-1) using Runnable 

Same code with Runnable as Anonymous Class passed as argument

ExecutorService exec = Executors.newFixedThreadPool(2);

//Instance of Runnable passed as Anonymous class
exec.execute(new Runnable() {
  public void run() {
    System.out.println("Hello world");
  }
});

exec.shutdown();

Simple Program using Executor Service taking Callable as Argument

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService objExecService = Executors.newFixedThreadPool(2);
        Future<String> objFuture = objExecService.submit(new HelloThread2());
        System.out.println(objFuture.get());
        objExecService.shutdown();
    }
}

Output

Hello World from Thread Name (pool-1-thread-1) using Callable

Using Lambda Expression as Submi

.
.
ExecutorService objExecService = Executors.newFixedThreadPool(2);

Future<String> objFuture = objExecService.submit(() -> {
  Thread.sleep(3000);
  return Thread.currentThread().getName();
});

System.out.println(objFuture.get());
.
.

The above could be rewritten in anonymous class as below

ExecutorService objExecService = Executors.newFixedThreadPool(2);

Future<String> objFuture = objExecService.submit(new Callable<String>() {
 @Override
 public String call() throws Exception {
   Thread.sleep(3000);
   return Thread.currentThread().getName();
 }
});

System.out.println(objFuture.get());
objExecService.shutdown();

Program for Creating Thread Pool and executing Task

ThreadPoolExample.java

public class ThreadPoolExample {
    public static void main(String args[]) {
       ExecutorService service = Executors.newFixedThreadPool(10); //create 10 worker threads in Thread Pool
       for (int i =0; i<100; i++){
           service.submit(new Task(i)); //submit that to be done 
       }
       service.shutdown();
    }  
}

Task.java

final class Task implements Runnable {
    private int taskId;  
    public Task(int id){
        this.taskId = id;
    }
  
    @Override
    public void run() {
        System.out.println("Task ID : " + this.taskId +" performed by " 
                           + Thread.currentThread().getName());
    }  
}
Task ID : 0 performed by pool-1-thread-1
Task ID : 3 performed by pool-1-thread-4
Task ID : 2 performed by pool-1-thread-3
Task ID : 1 performed by pool-1-thread-2
Task ID : 5 performed by pool-1-thread-6
Task ID : 4 performed by pool-1-thread-5

Thread – direction or path that is taken while a program is executed

  1. Below code would explain how dirty read happens when multiple thread(2 threads) tries to access Instance variable at once from two different class objects
  2. Below we have 3 classes, One for Printing Report and Other for removing the report printed. Third for tracking the report status
  3. The Output of the code would be Consistently 0 every time which is expected when the number of reports to be printed is in range of less than 100. However the output changes with more the no of reports to be printed. I.E. totalReportsToBePrinted = 10000000
  4. This happens because for lower value of reports to be printed the thread executes fast with out context switching however for higher values other thread(RemovePrintedReports thread takes control before PrintExcelReports get completed) takes control which leads to inconsistency
  5. The Same code would return 0 every time if one thread(PrintExcelReports) completes before other(RemovePrintedReports) as below when we use join and didn’t start both the threads at once.
    .
    .
            t1.start();
            t1.join();
            
            t2.start();
            t2.join();
    
    .
    .
    

PrintExcelReports.java

public class PrintExcelReports implements Runnable {
    TotalReportCount totalReportCount;

    public PrintExcelReports(TotalReportCount totalReportCount) {
        this.totalReportCount = totalReportCount;
    }

    int totalReportsToBePrinted = 1000000;

    @Override
    public void run() {
        for(int i=0;i<totalReportsToBePrinted;i++){
            totalReportCount.totalReportsCntVal -= i;
        }
    }
}

RemovePrintedReports.java

public class RemovePrintedReports implements Runnable {
    TotalReportCount totalReportCount;

    public RemovePrintedReports(TotalReportCount totalReportCount) {
        this.totalReportCount = totalReportCount;
    }

    int totalReportsToBePrinted = 1000000;

    @Override
    public void run() {
        for(int i=0;i<totalReportsToBePrinted;i++){
            totalReportCount.totalReportsCntVal -= i;
        }
    }
}

ReportCurrentStatus.java

public class ReportCurrentStatus {
    public static void main(String[] args) throws InterruptedException {
        TotalReportCount objTotalReportCount = new TotalReportCount();
        objTotalReportCount.totalReportsCntVal = 0;

        PrintExcelReports objPrinter1 = new PrintExcelReports(objTotalReportCount);
        RemovePrintedReports objPrinter2 = new RemovePrintedReports(objTotalReportCount);

        Thread t1 = new Thread(objPrinter1);
        Thread t2 = new Thread(objPrinter2);

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(objTotalReportCount.totalReportsCntVal);
    }
}

Output when totalReportsToBePrinted is greater than 1000

RANDOM NUMBER

Output when totalReportsToBePrinted is less than 100

0
  1. Thread Constructor(new Thread()) takes runnable as argument but not callable
  2. Thread instance can use either start() or run() methods to start execution of thread. start() method spawns new thread other than main to complete execution whereas run method uses the main thread to carry out execution.
  3. Managing Thread lifecycle is difficult hence ExecutionService framework was introduced
  4. When you run thread using ExecutionService it takes either instance of Ruunal

Thread Implementation Using Traditional Threads- Old Way

Task.java

public class Task implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello from Task.class, executed by " + currentThread().getName() + " thread");
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new Task());
        thread.run();
    }
}

Output

Hello from Task.class, executed by main thread

Thread Implementation Using ExecutionService – New Way
ExecutorService is an interface in Java that provides a higher-level replacement for using raw threads. It manages a pool of threads to handle asynchronous tasks more efficiently, without the need to manually create or manage individual threads.

Simple Program using Executor Service taking Runnable as Argument

ExecutorService is a framework which allows to create thread. Threads can be created from FixedThreadPool, CachedThreadPool and ScheduledThreadPool. submit() method takes runnable or callable object (Functional Interface Type) as argument.

Main.java

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);  
        executorService.submit(new Task());
    }
}

Output

Hello from Task.class, executed by pool-1-thread-1 thread

Why we need ExecutorService when we can directly call Thread using run() or start() method?

Problem with using old Thread Methods
Poor Resource Management i.e. It keep on creating new resource for every request. No limit to creating resource. Using Executor framework we can reuse the existing resources and put limit on creating resources.
Not Robust : If we keep on creating new thread we will get StackOverflowException exception consequently our JVM will crash.
Overhead Creation of time : For each request we need to create new resource. To creating new resource is time consuming. i.e. Thread Creating > task. Using Executor framework we can get built in Thread Pool.

  1. Use of Thread Pool reduces response time by avoiding thread creation during request or task processing.
  2. Use of Thread Pool allows you to change your execution policy as you need. you can go from single thread to multiple thread by just replacing ExecutorService implementation.
  3. Thread Pool in Java application increases stability of system by creating a configured number of threads decided based on system load and available resource.
  4. Thread Pool frees application developer from thread management stuff and allows to focus on business logic.

Runnable vs Callable
Using Runnable doesn’t returns any value whereas callable returns value to the main thread

HelloThread1.java

public class TaskRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Hello from "+ this.getClass().getSimpleName() + ", executed by " + currentThread().getName() + " thread");
    }
}

HelloThread2.java

public class TaskCallable implements Callable {
    @Override
    public String call() throws Exception {
        System.out.println("Hello from "+ this.getClass().getSimpleName() + ", executed by " + currentThread().getName() + " thread");
        return "Hello from Callable";
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        TaskRunnable objTaskRunnable = new TaskRunnable(); //Instance of Runnable
        TaskCallable objTaskCallable = new TaskCallable(); //Instance of Callable
       
        String strText =  objTaskCallable.call();
        System.out.println(strText); //I am a Blocking Operation
        objTaskRunnable.run();
    }
}

Output

Hello from TaskCallable, executed by main thread
Hello from Callable
Hello from TaskRunnable, executed by main thread

Now the same code can be executed using ExecutionService Framework by taking thread from thread pool as below

Main.java

public class Main {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(3);  // Pool of 3 threads// Submit tasks to the executor
        TaskCallable objTaskCallable = new TaskCallable();
        TaskRunnable objTaskRunnable = new TaskRunnable();

        Future<String> strText = executorService.submit(objTaskCallable);
        System.out.println(strText.get()); //I am a Blocking Operation
        executorService.submit(objTaskRunnable);

        executorService.shutdown();
    }
}

Output

Hello from TaskCallable, executed by pool-1-thread-1 thread
Hello from Callable
Hello from TaskRunnable, executed by pool-1-thread-2 thread
  1. NEW – a newly created thread that has not yet started the execution
  2. RUNNABLE – either running or ready for execution but it’s waiting for resource allocation
  3. BLOCKED – waiting to acquire a monitor lock to enter or re-enter a synchronized block/method
  4. WAITING – waiting for some other thread to perform a particular action without any time limit
  5. TIMED_WAITING – waiting for some other thread to perform a specific action for a specified period
  6. TERMINATED – has completed its execution

NEW Thread (or a Born Thread) is a thread that’s been created but not yet started.
It remains in this state until we start it using the start() method

NewState.java

public class NewState implements Runnable{
    public void run(){
        System.out.println("I am in new State");
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objThread = new Thread(new NewState());
       System.out.println(objThread.getState());
    }
}

Output

NEW

Runnable When we’ve created a new thread and called the start() method on that, it’s moved from NEW to RUNNABLE state. Threads in this state are either running or ready to run, but
they’re waiting for resource allocation from the system. In a multi-threaded environment, the Thread-Scheduler (which is part of JVM) allocates a fixed amount of time to each thread. So it runs for a particular amount of time, then leaves the control to other RUNNABLE threads.

RunnableState .java

public class RunnableState implements Runnable{
    public void run(){
        System.out.println("I would be in Runnable State");
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objRThread = new Thread(new RunnableState());
       objRThread.start();
       System.out.println(objRThread.getState());
    }
}

Output

RUNNABLE
I would be in Runnable State

This is the state of a dead thread. It’s in the TERMINATED state when it has either finished execution or was terminated abnormally.
TerminatedState.java

public class TerminatedState implements Runnable{
    public void run(){
        Thread objNewState = new Thread(new NewState());
        objNewState.start();
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objTState = new Thread(new TerminatedState());
       objTState.start();
       objTState.sleep(1000);
       System.out.println("T1 : "+ objTState.getState());
    }
}

Output

I am in new State
T1 : TERMINATED

A thread is in the BLOCKED state when it’s currently not eligible to run. It enters this state when it is waiting for a monitor lock and is trying to access a section of code that is locked by some other thread.
BlockedState.java

public class BlockedState implements Runnable{
    public void run(){
      blockedResource();
    }

    public static synchronized void blockedResource(){
        while(true){
            //Do Nothing
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread objB1Thread = new Thread(new BlockedState());
        Thread objB2Thread = new Thread(new BlockedState());

        objB1Thread.start();
        objB2Thread.start();

        Thread.sleep(1000);

        System.out.println(objB1Thread.getState());
        System.out.println(objB2Thread.getState());
        System.exit(0);
    }
}

Output

RUNNABLE
BLOCKED

A thread is in WAITING state when it’s waiting for some other thread to perform a particular action. According to JavaDocs, any thread can enter this state by calling any one of the following
object.wait() (or) thread.join() (or) LockSupport.park()

WaitingState.java

public class WaitingState implements Runnable{
    public void run(){
        Thread objWaitState = new Thread(new SleepState());

        objWaitState.start();

        try {
            objWaitState.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

SleepState.java

public class SleepState implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
       Thread objWaitingThread = new Thread(new WaitingState());
       objWaitingThread.start();
       objWaitingThread.sleep(1000);
       System.out.println("T1 : "+ objWaitingThread.getState());
       System.out.println("Main : "+Thread.currentThread().getState());
    }
}

Output

T1 : WAITING
Main : RUNNABLE

A thread is in TIMED_WAITING state when it’s waiting for another thread to perform a particular action within a stipulated amount of time. According to JavaDocs, there are five ways to put a thread on TIMED_WAITING state:
thread.sleep(long millis) (or) wait(int timeout) (or) wait(int timeout, int nanos) thread.join(long millis) (or) LockSupport.parkNanos (or) LockSupport.parkUntil

TimedWaitState.java

public class TimedWaitState implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread objTWState = new Thread(new TimedWaitState());
        objTWState.start();
        Thread.sleep(2000);
        System.out.println("T1 : "+ objTWState.getState());
    }
}

Output

T1 : TIMED_WAITING
  1. We use ReentrantLock for locking the Resource(totalSeats)
  2. Incase anything goes wrong (Exception being thrown etc.) you want to make sure the lock is released no matter what.
  3. Calling the reserveSeats method should be done inside separate threads

ReservationSystem.java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReservationSystem {
    private Integer totalSeats;
    private final Lock lock = new ReentrantLock();

    public ReservationSystem(Integer totalSeats){
        this.totalSeats = totalSeats;
    }

    public Integer getTotalSeats(){
        return totalSeats;
    }

    public void reserveSeats(String userName, int numOfSeats){
        lock.lock();

        try{
            if(numOfSeats >0 && totalSeats>numOfSeats){
                totalSeats -= numOfSeats;
                System.out.println(userName + " has reserved "+ numOfSeats + " with " + totalSeats + " still available");
            }else{
                System.out.println("Seats not Available");
            }
        }finally {
            lock.unlock();
        }
    }
}

BookSeat.java

public class BookSeat {
    public static void main(String[] args) {
        ReservationSystem objResSys = new ReservationSystem(100);

        System.out.println("Total available Seats "+ objResSys.getTotalSeats());

        Thread objThread1 = new Thread(() -> {objResSys.reserveSeats("User1", 10);});
        Thread objThread2 = new Thread(() -> {objResSys.reserveSeats("User2", 20);});
        Thread objThread3 = new Thread(() -> {objResSys.reserveSeats("User3", 5);});


        objThread1.start();
        objThread2.start();
        objThread3.start();

        try {
            objThread1.join();
            objThread2.join();
            objThread3.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Remaining available Seats "+ objResSys.getTotalSeats());
    }
}


Total available Seats 100
User2 has reserved 20 with 80 still available
User1 has reserved 10 with 70 still available
User3 has reserved 5 with 65 still available
Remaining available Seats 65

Banking System

  1. We have Bank Account with 2 Fields – balance and Account Number
  2. We have Transaction class implementing Runnable
  3. We create object for account with some initial balance and try to pass as parameter to runnable Transaction Object

BankAccount.java

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BankAccount {
    private Integer balance;
    private Integer accountNumber;

    private final Lock reLock = new ReentrantLock();

    public BankAccount(Integer balance, Integer accountNumber){
        this.balance = balance;
        this.accountNumber = accountNumber;
    }

    public void debitAmount(Integer amount){
        reLock.lock();

        try{
            balance -= amount;
        }finally {
            reLock.unlock();
        }

    }

    public void creditAmount(Integer amount){
        reLock.lock();

        try{
            balance += amount;
        }finally {
            reLock.unlock();
        }
    }

    public Integer getAccountNumber(){
        return this.accountNumber;
    }

    public Integer getBalance(){
        return this.balance;
    }

}

BankTransaction.java

public class BankTransaction implements Runnable{
    public Integer transAmount;
    public BankAccount bankAccount;

    public BankTransaction(Integer transAmount, BankAccount bankAccount){
        this.transAmount  = transAmount;
        this.bankAccount  = bankAccount;
    }


    @Override
    public void run() {
        if(transAmount >= 0){
            bankAccount.creditAmount(transAmount);
        }else{
            bankAccount.debitAmount(Math.abs(transAmount));
        }
    }
}

BankSystem.java

public class BankSystem {
    public static void main(String[] args) {
        BankAccount objAcc1 = new BankAccount(1000, 101);
        BankAccount objAcc2 = new BankAccount(2000, 102);

        Thread objThread1 = new Thread(new BankTransaction(50, objAcc1));
        Thread objThread2 = new Thread(new BankTransaction(-150, objAcc2));
        Thread objThread3 = new Thread(new BankTransaction(250, objAcc2));
        Thread objThread4 = new Thread(new BankTransaction(250, objAcc1));

        objThread1.start();
        objThread2.start();
        objThread3.start();
        objThread4.start();

        try{
            objThread1.join();
            objThread2.join();
            objThread3.join();
            objThread4.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("Final Balance in Account " + objAcc1.getAccountNumber() + " with balance " + objAcc1.getBalance());
        System.out.println("Final Balance in Account " + objAcc2.getAccountNumber() + " with balance " + objAcc2.getBalance());
    }
}

Output

Final Balance in Account 101 with balance 1300
Final Balance in Account 102 with balance 2100

Communication Models

  1. Synchronous + Blocking – Calling Customer Service and waiting for response online
  2. Asynchronous- Asking My Friend to Call Customer Service and I carry forward with my work
  3. Non-Blocking- Asking Call back from Customer Service and I carry forward with my work
  4. Asynchronous + Non Blocking – I am Calling customer Service and asking to call back My Friend and I carry forward with my work

request -> response
request -> streaming response (Stock Price in Stock Market App, Heart Beat for Health Check in Spring Boot Appp)
streaming request -> response (Using Google Docs and updating in drive in regular time intervals)
streaming request -> streaming response (Playing Game Online)

Reactive Stream Specification
Process Stream of Messages in a Non Blocking Asynchronous manner with Observer Design Pattern(Observe and React incase of change)

  1. Publisher: Emits a sequence of elements to its subscribers.
    void subscribe(Subscriber<? super T> s)
    
  2. Subscriber: Consumes elements provided by a Publisher.
    void onSubscribe(Subscription s)
    void onNext(T t)
    void onError(Throwable t)
    void onComplete()
    
  3. Subscription: Represents a one-to-one lifecycle of a Subscriber subscribing to a Publisher.
    void request(long n)
    void cancel()
    
  4. Processor: Represents a processing stage, which is both a Subscriber and a Publisher.Inherits both Subscriber and Publisher interfaces.

There would be one Publisher at Top, similar to root of tree and there would be 0 to N intermediate processors(subscriber + publisher) and there would be leaf Subscriber

Publisher, Subscriber and Subscription

public interface Publisher<T> {
   public void subscribe(Subscriber<? super T> s);
}
public interface Subscription {
   public void request(long n);
   public void cancel();
}
public interface Subscriber<T> {
   public void onSubscribe(Subscription s);
   public void onNext(T t);
   public void onError(Throwable t);
   public void onComplete();
}
  1. Publisher will have subscribe method through which we would pass the subscriber instance. The Subscription object would be returned by Publisher
  2. The Publisher hands over subscription object to Subscriber. Subscriber uses onSubscribe method to accept subscription.
  3. Subscriber could use subscription object using request method and could cancel subscription. Communication between Publisher and Subscriber happens using subscription object
  4. Subscriber can request N items using subscription object. The Publisher can iterate to N object using onNext method. Publisher only give 3 items if 3 items is requested by subscriber.
  5. If the Publisher has completed transferring all Items, then Publisher can call onComplete() method in Subscriber to notify the subscriber that its work is done
  6. Publisher calls onError() method to notify error.

  1. publisher does not produce data unless subscriber requests for it.
  2. publisher will produce only <= subscriber requested items. publisher can also produce 0 items!
  3. subscriber can cancel the subscription. producer should stop at that moment as subscriber is no longer interested in consuming the data
  4. producer can send the error signal

  1. PublisherImpl instance would call the subscribe method and pass instance of SubscriberImpl
  2. PublisherImpl subscribe method would inturn call the onSubscribe method using the instance of SubscriberImpl passed.
  3. SubscriberImpl would get the same subscription which it has been passed to publisherImpl subscribe method earlier
  4. PublisherImpl has following methods
    • subscribe – takes subscriber as argument and creates new subscription and notify the subscriber by calling the onSubscribe method
      public void subscribe(Subscriber subscriber) {
              var subscription = new SubscriptionImpl(subscriber);
              subscriber.onSubscribe(subscription);
      }
      
  5. SubscriberImpl has following methods
    • onSubscribe – To get the same subscription passed to publisher subscribe method. This is inturn called from publisherImpl
    • onNext – called from subscriptionImpl to pass the data during iteration
    • onError – called from subscriptionImpl during error
    • onComplete – called from when all data is completed
    @Override
        public void onSubscribe(Subscription subscription) {
            this.subscription = subscription;
        }
    
        @Override
        public void onNext(String emailId) {
            logger.info("Received {}", emailId);
        }
    
        @Override
        public void onError(Throwable throwable) {
            logger.info("---------------------------------------------");
            logger.info("Received error {}", throwable.getMessage());
        }
    
        @Override
        public void onComplete() {
            logger.info("Subscription ended");
        }
    
  6. SubscriptionImplhas following methods
    • request – you can request date using subscriptionImpl instance by passing no of records
    • cancel – cancel subscription
    @Override
        public void request(long requestedItemCnt) {
            if(isCancelled){
                return;
            }
    
            logger.info("Subscriber has requested {} items ", requestedItemCnt);
    
            if(requestedItemCnt >MAX_ITEMS){
                this.subscriber.onError(new RuntimeException(" Items requested is more than Total Items Available"));
                this.isCancelled = true;
                return;
            }
    
            //Check if all items(MAX_ITEMS) were sent
            for(int idx=0;idx<requestedItemCnt && count<MAX_ITEMS; idx++){
                count++;
                this.subscriber.onNext(this.faker.internet().emailAddress());
            }
    
            //If all items were sent complete subscription
            if(count == MAX_ITEMS){
                logger.info("No More data from Producer");
                this.subscriber.onComplete();
                isCancelled = true;
            }
        }
    
        @Override
        public void cancel() {
            logger.info("Cancelling Subscription... . . .");
            isCancelled = true;
        }
    

pom.xml

  <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-core</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor.netty</groupId>
            <artifactId>reactor-netty-core</artifactId>
        </dependency>
        <dependency>
            <groupId>io.projectreactor.netty</groupId>
            <artifactId>reactor-netty-http</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.javafaker</groupId>
            <artifactId>javafaker</artifactId>
            <version>${faker.version}</version>
        </dependency>

PublisherImpl.java

import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;

public class PublisherImpl implements Publisher {
    @Override
    public void subscribe(Subscriber subscriber) {
        var subscription = new SubscriptionImpl(subscriber);
        subscriber.onSubscribe(subscription);
    }
}

SubscriptionImpl.java

public class SubscriptionImpl implements Subscription {
    private static final Logger logger = LoggerFactory.getLogger(SubscriptionImpl.class);
    private final Subscriber<? super String> subscriber;
    private boolean isCancelled = false;
    private final Faker faker;

    private final int MAX_ITEMS = 10;
    private static int count = 0;


    public SubscriptionImpl(Subscriber subscriber){
        this.subscriber = subscriber;
        this.faker = Faker.instance();
    }

    @Override
    public void request(long requestedItemCnt) {
        if(isCancelled){
            return;
        }

        logger.info("Subscriber has requested {} items ", requestedItemCnt);

        if(requestedItemCnt >MAX_ITEMS){
            this.subscriber.onError(new RuntimeException(" Items requested is more than Total Items Available"));
            this.isCancelled = true;
            return;
        }

        //Check if all items(MAX_ITEMS) were sent
        for(int idx=0;idx<requestedItemCnt && count<MAX_ITEMS; idx++){
            count++;
            this.subscriber.onNext(this.faker.internet().emailAddress());
        }


        //If all items were sent complete subscription
        if(count == MAX_ITEMS){
            logger.info("No More data from Producer");
            this.subscriber.onComplete();
            isCancelled = true;
        }
    }

    @Override
    public void cancel() {
        logger.info("Cancelling Subscription... . . .");
        isCancelled = true;
    }
}

SubscriberImpl.java

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SubscriberImpl implements Subscriber<String> {
    private static final Logger logger = LoggerFactory.getLogger(SubscriberImpl.class);
    private Subscription subscription;

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
    }

    @Override
    public void onNext(String emailId) {
        logger.info("Received {}", emailId);
    }

    @Override
    public void onError(Throwable throwable) {
        logger.info("---------------------------------------------");
        logger.info("Received error {}", throwable.getMessage());
    }

    @Override
    public void onComplete() {
        logger.info("Subscription ended");
    }

    public Subscription getSubscription() {
        return subscription;
    }
}

Main.java

import org.mugil.publisher.PublisherImpl;
import org.mugil.subscriber.SubscriberImpl;

import java.time.Duration;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        getMessages();
    }

    public static void getMessages() throws InterruptedException {
        var objPublisher = new PublisherImpl();
        var objSubscriber = new SubscriberImpl();

        objPublisher.subscribe(objSubscriber);
        objSubscriber.getSubscription().request(1);
        Thread.sleep(Duration.ofSeconds(2));

        objSubscriber.getSubscription().request(2);
        Thread.sleep(Duration.ofSeconds(2));

        objSubscriber.getSubscription().request(3);
        Thread.sleep(Duration.ofSeconds(2));

        objSubscriber.getSubscription().cancel();

        objSubscriber.getSubscription().request(1);
    }
}

Output

18:00:57.240 [main] INFO org.mugil.publisher.SubscriptionImpl -- Subscriber has requested 1 items 
18:00:57.534 [main] INFO org.mugil.subscriber.SubscriberImpl -- Received jani.mante@gmail.com
18:00:59.537 [main] INFO org.mugil.publisher.SubscriptionImpl -- Subscriber has requested 2 items 
18:00:59.541 [main] INFO org.mugil.subscriber.SubscriberImpl -- Received sunny.quigley@yahoo.com
18:00:59.544 [main] INFO org.mugil.subscriber.SubscriberImpl -- Received hang.gutkowski@yahoo.com
18:01:01.546 [main] INFO org.mugil.publisher.SubscriptionImpl -- Subscriber has requested 3 items 
18:01:01.548 [main] INFO org.mugil.subscriber.SubscriberImpl -- Received malik.thiel@hotmail.com
18:01:01.549 [main] INFO org.mugil.subscriber.SubscriberImpl -- Received andre.purdy@gmail.com
18:01:01.550 [main] INFO org.mugil.subscriber.SubscriberImpl -- Received kim.greenfelder@gmail.com
18:01:03.560 [main] INFO org.mugil.publisher.SubscriptionImpl -- Cancelling Subscription... . . .