Published on

Java Concurrency: Thread Creation and Basic Concepts

Table of Contents

Concurrency in Our Systems

Modern computer systems are inherently concurrent, orchestrating multiple tasks with remarkable efficiency. In a single moment, your device:

  • Coordinates complex system processes
  • Renders real-time audio and video
  • Responds to user interactions instantaneously

Concurrency isn't just a technical concept—it's the invisible engine powering every digital interaction, enabling seamless, multi-threaded experiences that we now take for granted.

System and Hardware

  • Single-core CPU: Achieves concurrency by rapidly switching between tasks (context switching).
  • Multi-core CPU: Can run multiple tasks truly in parallel, one task per core at the exact same time.

[See: Concurrency vs. Parallelism in a multithreaded environment]

OS Model and Process

A process is an independent program in execution. Inside each process, there can be one or more threads—each with its own stack and program counter but sharing the same memory (heap). For more detail, check:

https://docs.oracle.com/javase/tutorial/essential/concurrency/procthread.html

Os Process and Threads

OS Thread Execution

Context Switches and Thread Lifecycle

What is a Context Switch?

A context switch is a fundamental operating system mechanism that allows multiple threads to share a single CPU core. It involves:

  • Saving the state of the currently running thread
  • Storing critical execution information
  • Loading another thread's state for execution

Thread State Components

Each thread carries a comprehensive execution context:

  • Program Counter: Current instruction location
  • CPU Registers: Temporary data storage
  • Stack Pointer: Current stack memory location
  • Thread-Specific Data: Local variables, method call history

Context Switch Lifecycle

Consider a real-world scenario with two threads: MainThread and WorkerThread

  1. Initial State

    • MainThread is running
    • Executing primary application logic
  2. Interrupt Trigger

    • Timer interrupt or higher-priority thread
    • Operating system scheduler intervenes
  3. State Preservation

    • Save MainThread current state
      • Program counter
      • Register values
      • Stack pointer
  4. Thread Selection

    • Scheduler chooses WorkerThread
    • Evaluates thread priority
    • Considers waiting time
  5. Context Restoration

    • Load WorkerThread saved state
    • Restore its program counter
    • Set up CPU registers
  6. Thread Execution

    • WorkerThread begins processing
    • Performs its designated tasks
  7. Potential Scenarios

    • Thread completes work
    • Thread blocks on I/O
    • Time slice expires
  8. Return to Main Thread

    • Similar preservation and restoration process
    • MainThread resumes execution

Thread Lifecycle States

Threads transition through multiple states:

  1. New: Thread created, not yet started
  2. Runnable: Ready to run, waiting for CPU time
  3. Running: Currently executing
  4. Blocked/Waiting: Suspended, waiting for resource
  5. Terminated: Execution completed

Multiple threads of varying types (long-running, quick tasks, IO-bound tasks) can run concurrently. Quicker tasks finish and exit the queue quickly, whereas long tasks re-enter the queue until completion. If you have hundreds of threads, there may be frequent context switches, leading to significant overhead. Too many threads can cause excessive management overhead rather than real work, consuming significant CPU time and memory.

IMPORTANT

Context switches are expensive:

  • Consume CPU cycles
  • Invalidate processor caches
  • Introduce overhead in thread management [See: Ideal/optimal thread count vs CPU cores]

NOTE

Performance Considerations:

  • Maintain an optimal number of threads
  • Use a suitable Thread Scheduling Algorithm
  • Goal: Minimize unnecessary switches

Scheduling

The thread scheduler (part of the OS) manages when threads get CPU time:

[For more See: Operating system thread scheduler]
https://web.cs.ucdavis.edu/~pandey/Teaching/ECS150/Lects/05scheduling.pdf

Common scheduling algorithms:

  • FCFS (First Come First Serve) – simple, but a long-running thread may starve others.
  • SJF (Shortest Job First) – short tasks get priority; longer tasks may suffer.
  • Priority Scheduling – threads have priorities; can be static or dynamic.

NOTE

  • Multi-threading alone doesn’t guarantee performance gains. Bottlenecks include the number of CPU cores, memory constraints, and switching overhead.
  • Each thread requires stack memory (often 512 KB or 1 MB by default in many systems).
  • With more threads than cores, the OS must time-slice CPU access among them. This adds scheduling and context-switch overhead.

Thread Basics in Java

The concept of threads is fundamental in Java. Java introduced basic concurrency support starting with version 5.0 in the java.util.concurrent packages.

A HelloWorld Program

Even the classic Hello World program runs on a thread called the main thread.

MainClass.java
public class MainClass {

    public static void main(String[] args) {
        System.out.println("Hello World");
        System.out.println("My name is: " + Thread.currentThread().getName());
        System.out.println("My id is: "+ Thread.currentThread().getId());
    }
}

Output:

Hello World
My name is: main
My id is: 1

Thread Creation

There are two main approaches to creating a thread in Java:

  1. Extending the Thread class
  2. Implementing the Runnable Interface

Let’s see some example code:

A1_ThreadCreation.java
public class A1_ThreadCreation {

    public static void main(String[] args) {
        System.out.println("01. Java Concurrency -- Thread Creation");

        // 1. Extends Thread
        HelloThread helloThread = new HelloThread();
        helloThread.start();

        // 2. Implements Runnable
        HelloThreadTwo helloThreadTwo = new HelloThreadTwo();
        Thread threadRunnable = new Thread(helloThreadTwo);
        threadRunnable.start();

        // Another common approach:
        // Passing Anonymous Runnable Object to Thread constructor
        Thread threadAnonymous = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello from thread created Using Anonymous Runnable Object");
            }
        });
        threadAnonymous.start();

        // Using a lambda (functional interface)
        Thread threadLambda = new Thread(() -> System.out.println("Hello from thread created Using Functional Interface"));
        threadLambda.start();
    }
}
    // 1. Extending Thread Class
    public static class HelloThread extends Thread {

        @Override
        public void run() {
            System.out.println("Hello from a Extends Thread implementation!");
        }

    }

    // 2. Implementing Runnable Interface
    public static class HelloThreadTwo implements Runnable {

        @Override
        public void run() {
            System.out.println("Hello from a Runnable thread Implementation!");
        }

    }

Output:

01. Java Concurrency -- Thread Creation
Hello from a Extends Thread implementation!
Hello from a Runnable thread Implementation!
Hello from thread created Using Anonymous Runnable Object
Hello from thread created Using Functional Interface

Interrupting a Running Thread

A2_ThreadInterrupts.java
public class A2_ThreadInterrupts {

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

        SomeTask someTask = new SomeTask();
        Thread threadSomeTask = new Thread(someTask);

        // If you want the thread to stop when the main thread stops, set it as a daemon:
        // threadSomeTask.setDaemon(true);

        threadSomeTask.start();

        /**
         * Let’s wait 3 seconds in the main thread, then interrupt "threadSomeTask".
         * main thread -> sleeps -> calls interrupt() on someTask’s thread
         */
        Thread.sleep(3000);
        threadSomeTask.interrupt();

        // --------------------------------------------------------------------------------

        LongProcessingTask longProcessingTask = new LongProcessingTask();
        Thread threadLongProcessing = new Thread(longProcessingTask);
        threadLongProcessing.start();

        /**
         * Immediately call interrupt on the second thread.
         * The actual interruption depends on whether and when
         * the thread checks 'Thread.interrupted()' or hits sleep().
         */
        threadLongProcessing.interrupt();
    }
    public static class SomeTask implements Runnable {

        @Override
        public void run() {
            System.out.println("----------- SomeTask ---------------");
            System.out.println("Executing some task in background ");

            try {
                System.out.println("Performing some operation. May take 10-20 Seconds !!!");
                Thread.sleep(10000);
                System.out.println("This task is executed after waiting for 10 seconds");

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class LongProcessingTask implements Runnable {

        @Override
        public void run() {

            System.out.println("----------- LongProcessingTask ---------------");
            System.out.println("I cant be interrupted. ( Need some conditions to stop the flow and 'Scheduled me out' ). ");
            while (true) {
                System.out.println("... I am performing Infinite sout ... ");

                // This thread checks interrupt status:
                if (Thread.interrupted()) {
                    System.out.println("Condition Met !!! I am interrupted.");
                    // Perform any cleanup if needed
                    return;
                }
            }
        }
    }
}

Thread Methods – join()

A2_ThreadMethods.java
public class A2_ThreadMethods {

    public static void main(String[] args) throws InterruptedException {
        /**
         * Inter-thread communication – see thread methods: join(), wait(), notify().
         *
         * Scenario:
         * 1) Send an SMS to a friend,
         * 2) Only after it's sent, check the phone balance.
         */

        AccountInfo accountInfo = new AccountInfo("CodeGrave", 200);

        // Threads
        SendSMS sendSMS = new SendSMS(accountInfo);
        Thread smsThread = new Thread(sendSMS);

        CheckPhoneBalance checkPhoneBalance = new CheckPhoneBalance(accountInfo);
        Thread checkBalance = new Thread(checkPhoneBalance);

        // Start SMS thread, then use join() to wait until it finishes
        smsThread.start();
        smsThread.join();

        // After SMS sending is done, check the balance
        checkBalance.start();
        checkBalance.join();
    }

    public static class SendSMS implements Runnable {
        private AccountInfo accountInfo;
        public SendSMS(AccountInfo accountInfo) {
            this.accountInfo = accountInfo;
        }

        @Override
        public void run() {
            System.out.println("----------- SendSMS ---------------");
            try {
                System.out.println("Sending SMS");
                Thread.sleep(5000);
                accountInfo.decreaseBalance(2); // SMS cost: $2
                System.out.println("SMS successfully sent to friend Rearc - 000-000-0000");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class CheckPhoneBalance implements Runnable {
        private AccountInfo accountInfo;
        public CheckPhoneBalance(AccountInfo accountInfo) {
            this.accountInfo = accountInfo;
        }

        @Override
        public void run() {
            System.out.println("----------- CheckPhoneBalance ---------------");
            try {
                System.out.println("Retrieving Phone Balance");
                Thread.sleep(500);
                System.out.println("Your remaining balance is: " + accountInfo.getCurrentBalance());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class AccountInfo {
        private String accountHolderName;
        private int currentBalance;

        public AccountInfo(String accountHolderName, int currentBalance) {
            this.accountHolderName = accountHolderName;
            this.currentBalance = currentBalance;
        }

        public int getCurrentBalance(){
            return this.currentBalance;
        }

        public void decreaseBalance(int am){
            this.currentBalance -= am;
        }
    }
}

Output (Without using join()):

----------- SendSMS ---------------
Sending SMS
----------- CheckPhoneBalance ---------------
Retrieving Phone Balance
Your remaining balance is: 200  # Even though we call send SMS , our balance is still 200 ?

SMS successfully send to friend Rearc - 000-000-0000 # Since SMS takes some time, we need to wait .

# How can we make the main thread wait after invoking send SMS thread? >> We do by join().

Output (Using join()):

----------- SendSMS ---------------
Sending SMS
SMS successfully send to friend Rearc - 000-000-0000
----------- CheckPhoneBalance ---------------
Retrieving Phone Balance
Your remaining balance is: 198 # As expected, the balance was decreased after the SMS was sent.

Conclusion

Understanding the fundamentals of Java concurrency is crucial for developing efficient, responsive, and scalable applications. By mastering thread creation, lifecycle management, and synchronization techniques, you can build robust concurrent systems.

Ready to dive deeper? Continue your Java concurrency journey with Part 2: Advanced Concurrency Techniques and Resource Sharing.