Core Java Part 4 : Multithreading & Concurrency Introduction
- 🚀 Introduction
- 🧩 1. What is a Thread?
- ⚙️ 2. Creating Threads in Java
- 🌀 3. Thread Lifecycle & State Transitions
- 🪄 4. Thread Synchronization
- 🔁 5.
wait(),notify(), andnotifyAll() - 🔒 6. ReentrantLock — A Modern Alternative to synchronized
- ⚙️ 7. Callable, Future & ExecutorService
- ⚠️ 8. Common Concurrency Problems
- 🧭 9. Key Takeaways
🚀 Introduction
Have you ever used an app that froze while loading something? That’s what happens when a program does everything one step at a time — no multitasking.
Modern systems demand speed, responsiveness, and parallel execution, and that’s exactly what multithreading and concurrency enable.
🧩 1. What is a Thread?
A thread is simply a path of execution inside our program.
When we start a Java application, one main thread begins executing the main() method.
But we can create more threads to do other tasks simultaneously.
🧠 Analogy
Imagine a restaurant:
- The restaurant (our program) can hire multiple waiters (threads).
- Each waiter can handle one customer (task) independently.
- They all share the same kitchen (memory).
This sharing makes things fast — but also dangerous if not managed properly (we’ll talk about that soon).
⚙️ 2. Creating Threads in Java
There are two main ways to create a thread in Java:
✅ Method 1: Extending Thread class
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Example1 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // Start the thread (calls run() internally)
}
}
🟢 Output:
Thread is running: Thread-0
✅ Method 2: Implementing Runnable interface
This is more flexible and recommended for most cases.
class MyTask implements Runnable {
public void run() {
System.out.println("Running task in: " + Thread.currentThread().getName());
}
}
public class Example2 {
public static void main(String[] args) {
Thread t1 = new Thread(new MyTask());
t1.start();
}
}
🟩 Why Runnable? Because Java allows multiple threads to share the same Runnable and it’s easier to manage when using thread pools (discussed later).
🌀 3. Thread Lifecycle & State Transitions
Every thread in Java passes through well-defined states during its life.
| State | Meaning | Trigger |
|---|---|---|
| NEW | Thread created but not started | new Thread() |
| RUNNABLE | Ready to run (waiting for CPU time) | start() |
| RUNNING | Actively executing | JVM Scheduler |
| WAITING / TIMED_WAITING | Waiting for another thread | wait(), join(), sleep() |
| TERMINATED | Finished execution | run() completed |
🔍 Example
public class ThreadStates {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING
t.join();
System.out.println(t.getState()); // TERMINATED
}
}
🪄 4. Thread Synchronization
When multiple threads share the same memory (like shared variables or objects), things can go wrong if they try to modify it at the same time.
This is called a race condition.
❌ Example of Race Condition
class Counter {
int count = 0;
public void increment() {
count++;
}
}
public class RaceConditionDemo {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count: " + counter.count);
}
}
🟠 Expected: 2000
🔴 Actual (often): something less!
Why? Because both threads read and update the same variable at the same time.
✅ Solution: synchronized Keyword
synchronized ensures that only one thread can access a block or method at a time.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Now, every increment happens one at a time, preserving correctness.
💡 Under the Hood
When a thread enters a synchronized block, it acquires a lock on the object.
No other thread can enter any synchronized block on that object until the lock is released.
🔁 5. wait(), notify(), and notifyAll()
Sometimes, threads need to communicate and coordinate.
Example: Producer-Consumer problem. One thread produces items, another consumes them. The consumer must wait if there’s nothing to consume.
🧠 Example: Producer–Consumer with wait() & notify()
class SharedBuffer {
private int data;
private boolean hasData = false;
public synchronized void produce(int value) throws InterruptedException {
while (hasData) wait(); // wait until consumed
data = value;
hasData = true;
System.out.println("Produced: " + value);
notify(); // wake up consumer
}
public synchronized int consume() throws InterruptedException {
while (!hasData) wait(); // wait until produced
hasData = false;
System.out.println("Consumed: " + data);
notify(); // wake up producer
return data;
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
SharedBuffer buffer = new SharedBuffer();
Thread producer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try { buffer.produce(i); } catch (InterruptedException e) {}
}
});
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try { buffer.consume(); } catch (InterruptedException e) {}
}
});
producer.start();
consumer.start();
}
}
🧩 Output (interleaved):
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
...
🔒 6. ReentrantLock — A Modern Alternative to synchronized
Java’s ReentrantLock (from java.util.concurrent.locks) gives more control than synchronized.
We can:
- Try to acquire a lock without waiting forever (
tryLock()) - Interrupt waiting threads
- Use multiple condition variables (like advanced wait/notify)
Example
import java.util.concurrent.locks.ReentrantLock;
class SafeCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // acquire lock
try {
count++;
} finally {
lock.unlock(); // always release lock
}
}
public int getCount() {
return count;
}
}
🧠 Tip:
Always use the try-finally pattern so locks are released even if exceptions occur.
⚙️ 7. Callable, Future & ExecutorService
Creating threads manually doesn’t scale for large systems.
Java provides a thread pool framework — ExecutorService — which manages threads efficiently.
✅ Example: Using ExecutorService
import java.util.concurrent.*;
public class ExecutorExample {
public static void main(String[] args) throws Exception {
ExecutorService service = Executors.newFixedThreadPool(3);
// Submit a Runnable
service.submit(() -> System.out.println("Runnable executed"));
// Submit a Callable (returns a value)
Future<Integer> future = service.submit(() -> {
Thread.sleep(1000);
return 42;
});
System.out.println("Result from Callable: " + future.get()); // waits if needed
service.shutdown();
}
}
Here:
ExecutorServicereuses threads instead of creating new ones each time.Callableis likeRunnablebut returns a result.Futurelets us retrieve that result later.
⚠️ 8. Common Concurrency Problems
When working with threads, we’ll encounter these classic issues:
🧱 Deadlock
Two threads wait for each other’s lock — forever.
Example:
- Thread A locks Object1, waits for Object2.
- Thread B locks Object2, waits for Object1.
Result → Both stuck forever.
💡 Avoidance tip: Always acquire locks in a consistent order.
🔄 Livelock
Threads keep responding to each other and never make progress.
Analogy: Two people politely trying to pass each other in a hallway — “After you!” “No, after you!” — and they keep switching sides endlessly.
⏳ Starvation
One thread never gets CPU time because others hog the resource.
Fix: Use fair locking or thread priorities properly.
🧍♂️ Thread Safety
A class is thread-safe if multiple threads can use it without breaking behavior. We can achieve thread-safety using:
- Immutability
- Synchronization
- Concurrent Collections
- Atomic variables (
AtomicInteger,AtomicReference, etc.)
🧭 9. Key Takeaways
| Concept | Meaning | Example |
|---|---|---|
| Thread | Independent unit of execution | Thread, Runnable |
| Thread lifecycle | Creation → Run → Wait → Death | getState() |
| Synchronization | Protect shared data | synchronized, ReentrantLock |
| Thread communication | Coordinate between threads | wait(), notify() |
| ExecutorService | Thread pool management | submit(), Future |
| Common issues | Deadlock, livelock, starvation | Use ordered locking & fairness |
Multithreading is like hiring more workers for our app — it can make it blazing fast, but without discipline, it can also cause chaos.
Start small. Experiment with:
- Creating threads using
Runnable - Protecting shared data with
synchronized - Using modern APIs like
ExecutorService - And finally, observe how threads behave using logs or debuggers.