- 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 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
Initial State
MainThread
is running- Executing primary application logic
Interrupt Trigger
- Timer interrupt or higher-priority thread
- Operating system scheduler intervenes
State Preservation
- Save
MainThread
current state- Program counter
- Register values
- Stack pointer
- Save
Thread Selection
- Scheduler chooses
WorkerThread
- Evaluates thread priority
- Considers waiting time
- Scheduler chooses
Context Restoration
- Load
WorkerThread
saved state - Restore its program counter
- Set up CPU registers
- Load
Thread Execution
WorkerThread
begins processing- Performs its designated tasks
Potential Scenarios
- Thread completes work
- Thread blocks on I/O
- Time slice expires
Return to Main Thread
- Similar preservation and restoration process
MainThread
resumes execution
Thread Lifecycle States
Threads transition through multiple states:
- New: Thread created, not yet started
- Runnable: Ready to run, waiting for CPU time
- Running: Currently executing
- Blocked/Waiting: Suspended, waiting for resource
- 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.
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:
- Extending the Thread class
- Implementing the Runnable Interface
Let’s see some example code:
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
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()
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.