Master Core Java Programming From Scratch

Clear, interactive, and structured coding lessons designed for absolute beginners.

Module 8

Multithreading in Java

Learn how Java creates and manages multiple threads for concurrent execution.

๐Ÿงต

1. What is Multithreading?

Multithreading is the process of executing multiple threads concurrently within a single Java application.

A thread is a lightweight unit of execution. Multiple threads can work on different tasks while sharing the same process memory.

Key idea: Multithreading allows an application to perform multiple tasks concurrently.
Java
public class Simple {
    public static void main(String[] args) {

        System.out.println("Main thread is running");

        Thread thread = new Thread(() -> {
            System.out.println("Worker thread is running");
        });

        thread.start();
    }
}

2. Process vs Thread

Process Thread
Independent program in execution Lightweight unit inside a process
Has its own memory space Shares process memory
More expensive to create Less expensive to create
Communication is relatively expensive Communication is easier through shared memory

3. Creating a Thread by Extending Thread

One way to create a thread is by extending the Thread class and overriding its run() method.

Java
class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

public class Simple {
    public static void main(String[] args) {

        MyThread thread = new MyThread();

        thread.start();
    }
}
Important: Call start() to create a new thread. Calling run() directly does not create a new thread.

4. Creating a Thread Using Runnable

Implementing Runnable is generally more flexible because Java allows a class to extend only one class.

Java
class Task implements Runnable {

    @Override
    public void run() {
        System.out.println("Task is running");
    }
}

public class Main {
    public static void main(String[] args) {

        Runnable task = new Task();

        Thread thread = new Thread(task);

        thread.start();
    }
}

5. Creating Threads Using Lambda Expressions

Since Runnable is a functional interface, it can be used with a lambda expression.

Java
public class Main {
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            System.out.println("Processing live mock test updates for CIIT students ๐Ÿค“๐Ÿ‘จโ€๐Ÿซ...!");
        });

        thread.start();
    }
}

6. start() vs run()

start() run()
Creates a new thread Runs like a normal method
Execution can happen concurrently No new thread is created
Preferred for starting a thread Used when direct method execution is intended
Java
Thread thread = new Thread(() -> {
    System.out.println("Worker");
});

thread.start();   // Creates a new thread

// thread.run();  // Executes directly on current thread

7. Thread Name and ID

Every thread has a name and an ID.

Java
public class Program {
    public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            System.out.println(
                "Name: " + Thread.currentThread().getName()
            );

            System.out.println(
                "ID: " + Thread.currentThread().getId()
            );
        });

        thread.setName("Worker-1");

        thread.start();
    }
}

8. Thread.sleep()

Thread.sleep() pauses the current thread for a specified amount of time.

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

        System.out.println("Start");

        Thread.sleep(2000);

        System.out.println("After 2 seconds");
    }
}

9. join()

The join() method makes one thread wait until another thread completes.

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

        Thread worker = new Thread(() -> {

            for (int i = 1; i <= 5; i++) {
                System.out.println("Worker: " + i);
            }
        });

        worker.start();

        worker.join();

        System.out.println("Worker completed");
    }
}

10. Daemon Threads

A daemon thread is a background thread that normally does not keep the JVM alive after all user threads have finished.

Java
public class Main {
    public static void main(String[] args) {

        Thread daemon = new Thread(() -> {

            while (true) {
                System.out.println("Background task");
            }
        });

        daemon.setDaemon(true);

        daemon.start();

        System.out.println("Main completed");
    }
}

11. Thread Priority

Java provides thread priorities from 1 to 10. The default priority is 5.

Java
Thread thread = new Thread(() -> {
    System.out.println("Worker");
});

thread.setPriority(Thread.MAX_PRIORITY);

thread.start();
Thread priority is only a scheduling hint. It should not be used to guarantee execution order.

12. Thread Lifecycle

A Java thread can move through several states:

  • NEW - Thread object is created but not started.
  • RUNNABLE - Thread is ready or running.
  • BLOCKED - Waiting to acquire a monitor lock.
  • WAITING - Waiting indefinitely for another thread.
  • TIMED_WAITING - Waiting for a specified period.
  • TERMINATED - Thread execution has completed.
Java
Thread thread = new Thread(() -> {
    System.out.println("Running");
});

System.out.println(thread.getState());

thread.start();

System.out.println(thread.getState());

13. Race Condition

A race condition occurs when multiple threads access shared data concurrently and the final result depends on the timing of execution.

Java
class Counter {

    int count = 0;

    void increment() {
        count++;
    }
}

If several threads call increment() at the same time, updates can be lost.

14. synchronized Methods

The synchronized keyword provides mutual exclusion so that only one thread can execute the synchronized method on the same object at a time.

Java
class Counter {

    private int count = 0;

    synchronized void increment() {
        count++;
    }

    int getCount() {
        return count;
    }
}

15. synchronized Block

A synchronized block allows you to protect only the critical section of code.

Java
class Counter {

    private int count = 0;

    void increment() {

        synchronized (this) {
            count++;
        }
    }

    int getCount() {
        return count;
    }
}

16. Object Locks

Every Java object can act as a monitor lock. Synchronized instance methods use the object's monitor.

Java
class BankAccount {

    private double balance = 1000;

    synchronized void withdraw(double amount) {

        if (balance >= amount) {
            balance -= amount;
        }
    }

    synchronized double getBalance() {
        return balance;
    }
}

17. ReentrantLock

ReentrantLock provides more explicit locking control than the synchronized keyword.

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

class Counter {

    private int count = 0;

    private final ReentrantLock lock =
        new ReentrantLock();

    void increment() {

        lock.lock();

        try {
            count++;
        }
        finally {
            lock.unlock();
        }
    }
}
Important: Always release an explicit lock in a finally block.

18. ReadWriteLock

ReadWriteLock allows multiple readers at the same time while keeping writes exclusive.

Java
import java.util.concurrent.locks.*;

class DataStore {

    private final ReadWriteLock lock =
        new ReentrantReadWriteLock();

    private String data = "CIIT";

    String read() {

        lock.readLock().lock();

        try {
            return data;
        }
        finally {
            lock.readLock().unlock();
        }
    }

    void write(String value) {

        lock.writeLock().lock();

        try {
            data = value;
        }
        finally {
            lock.writeLock().unlock();
        }
    }
}

19. volatile Keyword

The volatile keyword provides visibility guarantees for a variable between threads.

Java
class Worker {

    private volatile boolean running = true;

    void stop() {
        running = false;
    }

    void work() {

        while (running) {
            // perform work
        }
    }
}
Note: volatile does not make compound operations such as count++ atomic.

20. Atomic Classes

Atomic classes provide thread-safe operations without manually using synchronized blocks for many common counters and state variables.

Java
import java.util.concurrent.atomic.AtomicInteger;

class Counter {

    private final AtomicInteger count =
        new AtomicInteger(0);

    void increment() {
        count.incrementAndGet();
    }

    int getCount() {
        return count.get();
    }
}

21. ExecutorService

ExecutorService manages a pool of worker threads and is usually preferred over manually creating many threads.

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

public class Program {

    public static void main(String[] args) {

        ExecutorService executor =
            Executors.newFixedThreadPool(3);

        executor.submit(() -> {
            System.out.println("CIIT Server: Generating student report cards โ„น๏ธ.");

        });

        executor.submit(() -> {
            System.out.println("CIIT Server: Sending automated attendance SMS to parents ๐Ÿ˜’โ˜Ž๏ธ.");

        });

        executor.shutdown();
    }
}

22. Callable

Callable is similar to Runnable, but it can return a result and throw checked exceptions.

Java
import java.util.concurrent.*;

public class Main {

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

        ExecutorService executor =
            Executors.newSingleThreadExecutor();

        Callable<Integer> task = () -> {
            return 10 + 20;
        };

        Future<Integer> result =
            executor.submit(task);

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

        executor.shutdown();
    }
}

23. Future

A Future represents the result of an asynchronous computation.

Java
ExecutorService executor =
    Executors.newSingleThreadExecutor();

Future<String> future =
    executor.submit(() -> {

        Thread.sleep(1000);

        return "Task completed";
    });

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

executor.shutdown();

24. CountDownLatch

CountDownLatch allows one or more threads to wait until a set of operations has completed.

Java
import java.util.concurrent.CountDownLatch;

CountDownLatch latch =
    new CountDownLatch(2);

Thread t1 = new Thread(() -> {

   System.out.println("CIIT Server 1: Student Records Database initialized ๐Ÿ‘จโ€๐Ÿซ๐Ÿคž .");

    latch.countDown();
});

Thread t2 = new Thread(() -> {

    System.out.println("CIIT Server 2: Fee Payment Portal initialized ๐Ÿ˜Šโ˜Ž๏ธ.");

    latch.countDown();
});

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

latch.await();

System.out.println("All CIIT core systems ready. Opening Main Admission Portal ๐Ÿค“๐Ÿ‘จโ€๐Ÿซ!");

25. Semaphore

A Semaphore controls access to a limited number of resources.

Java
import java.util.concurrent.Semaphore;

Semaphore semaphore =
    new Semaphore(2);

semaphore.acquire();

try {
    System.out.println("Using resource");
}
finally {
    semaphore.release();
}

26. Deadlock

A deadlock occurs when two or more threads wait forever for resources held by each other.

Concept
Thread 1:
Lock A โ†’ waits for Lock B

Thread 2:
Lock B โ†’ waits for Lock A
Avoid deadlocks by: keeping lock ordering consistent, minimizing lock scope, and avoiding unnecessary nested locks.

27. Starvation

Starvation occurs when a thread continuously fails to obtain the CPU or required resources because other threads keep getting priority.

28. Livelock

In a livelock, threads are active but continuously respond to each other without making useful progress.

29. Concurrent Collections

Java provides thread-safe collections designed for concurrent applications.

Java
import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Integer> map =
    new ConcurrentHashMap<>();

map.put("Java", 100);
map.put("Spring", 200);

System.out.println(map.get("Java"));

Common concurrent collections include:

  • ConcurrentHashMap
  • CopyOnWriteArrayList
  • BlockingQueue
  • ConcurrentLinkedQueue

30. Immutability and Thread Safety

Immutable objects cannot change after creation. They are naturally safer to share between multiple threads.

Java
public final class Employee {

    private final String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

31. Virtual Threads

Modern Java provides virtual threads, which are lightweight threads designed to make high-concurrency applications easier to build.

Java
public class Main {

    public static void main(String[] args) {

        Thread.startVirtualThread(() -> {

           System.out.println("
           CIIT high-concurrency student portal request running on an ultra-lightweight virtual thread ๐Ÿค“๐Ÿ‘€."

            );

        });
    }
}
Modern Java: Virtual threads are especially useful for applications with large numbers of concurrent I/O-bound tasks.

32. Example ๐ŸŒ๐Ÿค“

Imagine an online shopping application processing multiple orders. Different tasks can execute concurrently:

  • Validate payment
  • Check inventory
  • Send confirmation email
  • Update order status
  • Generate notification
Java
ExecutorService executor =
    Executors.newFixedThreadPool(4);

executor.submit(() ->
   System.out.println("CIIT System: Processing admission fee payment confirmation โ„น๏ธ.");
);

executor.submit(() ->
    System.out.println("CIIT System: Checking batch seat availability and updates ๐Ÿ‘€.");
);

executor.submit(() ->
    System.out.println("CIIT System: Sending welcome kit email with student portal login info ๐Ÿ˜Š.");
);

executor.submit(() ->
    System.out.println("CIIT System: Allocating roll number and updating student roster status ๐Ÿคทโ€.");
);

executor.shutdown();

33. Multithreading Best Practices

  • Prefer ExecutorService for managing thread pools.
  • Keep shared mutable state to a minimum.
  • Prefer immutable objects where possible.
  • Keep synchronized sections small.
  • Always release explicit locks.
  • Avoid unnecessary thread creation.
  • Use concurrent collections when appropriate.
  • Do not depend on thread priority for program correctness.
  • Use virtual threads for suitable high-concurrency workloads.
  • Design carefully to avoid deadlocks.

34. Interview Questions

Multithreading is the concurrent execution of multiple threads within a process.

start() creates a new thread and eventually invokes run(). Calling run() directly executes the method on the current thread.

A race condition happens when multiple threads access shared mutable data concurrently and the result depends on timing.

Synchronization controls concurrent access to shared resources so that unsafe simultaneous modifications are prevented.

Deadlock occurs when threads wait indefinitely for locks or resources held by each other.

ExecutorService is a framework for submitting and managing asynchronous tasks using reusable worker threads.

Virtual threads are lightweight Java threads designed to support applications with very large numbers of concurrent tasks.

Summary

In this lesson, you learned:

  • What multithreading is
  • Process vs thread
  • Creating threads with Thread and Runnable
  • Lambda-based threads
  • start(), run(), sleep(), and join()
  • Thread lifecycle and priorities
  • Race conditions and synchronization
  • Locks and atomic classes
  • ExecutorService, Callable, and Future
  • CountDownLatch and Semaphore
  • Deadlock, starvation, and livelock
  • Concurrent collections
  • Immutability and thread safety
  • Modern virtual threads