Java Threads

Java provides built-in support for multithreaded programming, allowing developers to create applications that can perform multiple operations simultaneously.

Concurrency in Java

Concurrency is the ability of a program to execute multiple tasks during overlapping time periods, giving the appearance that they are running simultaneously. In Java, concurrency is achieved through multithreading, which allows multiple threads to run concurrently within a single process. This improves the efficiency and performance of applications, especially on multi-core processors.

Objectives

After completing this lecture, you should be able to answer the following questions:

  1. What is the difference between concurrency and parallelism?
  2. How do threads differ from processes?
  3. What are the different ways to create threads in Java?
  4. How do thread states work and what are their lifecycle transitions?
  5. What are thread priorities and what are their limitations?

Threads vs. Processes

  • Processes: Independent execution units with separate memory spaces.
  • Threads: Lightweight sub-processes sharing the same memory space within a process.

All processes begin with at least one thread executing the program’s main() method. Multiple threads within a process execute concurrently and share access to the process’s global memory space. However, each thread maintains its own execution state, including a private call stack and thread-local variables.

Concurrency vs Parallelism

  • Concurrency is a way to structure a program by breaking it into independently executing tasks.

    • Concurrency is about dealing with multiple tasks at once.
    • Concurrency does not necessarily mean tasks run simultaneously; instead, it interleaves their execution by switching between them rapidly.
    • In Java, you can achieve concurrency using threads provided in the java.util.concurrent package.
    • Application Examples:
      • User interfaces remaining responsive while performing background tasks (e.g., downloading files).
      • Web servers handling multiple requests.
  • Parallelism is about executing multiple tasks simultaneously to leverage multiple CPU cores.

    • Parallelism is about doing multiple tasks at once.
    • In Java, Parallelism can be achieved by running multiple threads or processes on different CPU cores provided in the java.util.concurrent package.
    • Application Examples:
      • Data processing tasks that can be divided into independent subtasks.
      • Scientific computations and simulations. For example, a data processing application can split a large dataset into smaller chunks and process each chunk in parallel on different cores, significantly reducing the overall processing time.

Rob Pike, one of the founders of Go, famously explained the difference between concurrency and parallelism in his talk Concurrency is not Parallelism.

Thread Creation Mechanisms in Java

There are four ways to create threads in Java.

  1. Extending Thread Class: Inherit from the base Thread class and override the run() method.
  2. Implementing Runnable Interface: Implement the Runnable interface and override the run() method.
  3. Anonymous Inner Class: Create threads using anonymous inner classes.
  4. Lambda Expression: Use lambda expressions to create threads (Java 8+).

Each method has its own pros and cons, which are discussed in detail below.

1. Extending Thread Class

Overview

In this approach, we create a custom thread by inheriting from the base Thread class and overriding the run() method. We then instantiate the Thread class and call the start() method to execute the task in a separate thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running via class inheritance");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // Starts the thread execution
    }
}

IMPORTANT NOTE: ⚠️⛔️⚠️⛔️ Always call thread.start()

When we want to execute a task in a new thread (i.e., concurrently with the main thread), we call thread.start(). We do not call the instance method thread.run() directly as this will execute the task in the current main thread rather than creating a new thread. Therefore, we always use the start method to create a new thread of execution and execute the task in the new thread concurrently with the main thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running via class inheritance");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // ✅ Correct: This will start a new thread
        thread.run();  // ❌ Incorrect: This will not start a new thread and will run in the main thread
    }
}

Pros

  • Simple and straightforward implementation
  • Direct control over thread behavior
  • Intuitive for object-oriented programming

Cons

  • Java doesn’t support multiple inheritance, so this approach limits flexibility
  • Less flexible compared to other threading methods

2. Implementing Runnable Interface

Overview

In this approach, we create a custom thread by implementing the Runnable interface and overriding the run() method. We then instantiate the Thread class, pass our Runnable implementation as an argument, and call the start() method to execute the task in a separate thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread running via Runnable interface");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

Pros

  • Allows class to extend another class if needed
  • More flexible and follows the principle of favoring composition over inheritance
  • Separates thread execution logic from thread management

Cons

  • Requires explicit thread instantiation
  • Slightly more verbose than the Thread class inheritance approach

3. Anonymous Inner Class

Overview

Creates threads using anonymous inner classes, providing inline thread definition without creating a separate named class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class AnonymousThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                System.out.println("Thread via anonymous inner class");
            }
        });
        thread.start();
    }
}

Pros

  • Compact and readable
  • No need for separate class definition
  • Quick for simple, one-off thread implementations

Cons

  • Can become cluttered with complex nested logic
  • Less reusable

4. Lambda Expression (Java 8+)

Overview

Modern Java allows creating threads using lambda expressions, simplifying the syntax. Please refer to the lecture note on lambda expressions.

4.1) Directly passing lambda to Thread constructor

We can directly passes a lambda expression to the Thread constructor:

1
2
3
4
5
6
7
8
public class LambdaThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread via lambda expression");
        });
        thread.start();
    }
}

4.2) Assigning lambda to Runnable variable:

We can also assign a lambda expression to a Runnable variable before passing it to the Thread constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class LambdaRunnableExample {
    public static void main(String[] args) {
        // Assigning lambda expression to a Runnable variable
        Runnable runnable = () -> {
            System.out.println("Thread via lambda assigned to Runnable variable");
        };
        
        // Passing the Runnable variable to Thread constructor
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Pros

  • Extremely concise
  • Improves code readability
  • No boilerplate code

Cons

  • Requires Java 8 or higher
  • May reduce code clarity for complex implementations
  • Perhaps less readable and not understood by junior team members

Common Thread Methods

MethodDescriptionExample
start()Begins thread execution, calls the run() methodthread.start();
run()Contains code to be executed in the threadThread thread = new Thread(() -> { /* code */ });
sleep(long millis)Pauses thread execution for specified millisecondsThread.sleep(1000); // sleep 1 second
join()Waits for thread to diethread.join(); // wait for thread completion
interrupt()Interrupts a thread in sleep/wait statethread.interrupt();
isAlive()Tests if thread is still runningif(thread.isAlive()) { /* code */ }
getName()Returns this thread’s nameString name = thread.getName();
setName(String name)Sets this thread’s namethread.setName("WorkerThread");
getPriority()Returns thread priority (1-10)int priority = thread.getPriority();
setPriority(int p)Sets thread priority (1-10)thread.setPriority(6);
getState()Return this thread’s stateThread.State state = thread.getState();
currentThread()Returns a reference to the currently executing threadThread current = Thread.currentThread();

Thread Lifecycle States

According to the Java Documentation, a thread can be in one of the following states: New, Runnable, Blocked, Waiting, Timed Waiting, or Terminated. The following table summarizes each thread state, its meaning, and the methods that trigger transitions to that state:

StateDescriptionMethods
NewA thread that has not yet started.Thread constructor new Thread()
RunnableA thread executing in the Java virtual machine.thread.start()
BlockedA thread that is blocked waiting for a monitor lock.Attempting to enter a synchronized block/method
WaitingA thread that is waiting indefinitely for another thread to perform a particular action.Thread.join(), Object.wait()
Timed WaitingA thread that is waiting for another thread to perform an action for up to a specified waiting time.Thread.sleep(), Object.wait(timeout), Thread.join(timeout)
TerminatedA thread that has exited.run() method completes, stop()

1. New (Created) State

  • Thread is created but not yet started
  • start() method has not been called yet
  • Thread is in memory but not executing
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class NewStateDemo {
    public static void main(String[] args) {
        // Thread is in NEW state
        Thread thread = new Thread(() -> {
            System.out.println("Thread is executing");
        });

        // Thread state is still NEW
        System.out.println("Thread State: " + thread.getState());
    }
}

Output:

Thread State: NEW

2. Runnable State

  • start() method has been called
  • When a thread is in the RUNNABLE state, it can be either:
    • Actually executing on the CPU
    • Ready to run but waiting for CPU allocation
  • Java doesn’t distinguish between these two situations (actively running vs ready for execution) because the JVM delegates the actual scheduling of thread execution to the underlying operating system.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class RunnableStateDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(i+ "- Running: " +  Thread.currentThread().getName() + " " +
                        Thread.currentThread().getState());
            }
        });

        thread.start();  // Moves to RUNNABLE state
        System.out.println("Thread State: " + thread.getState());
    }
}

Output:

Thread State: RUNNABLE
0- Running: Thread-0 RUNNABLE
1- Running: Thread-0 RUNNABLE
2- Running: Thread-0 RUNNABLE
3- Running: Thread-0 RUNNABLE
4- Running: Thread-0 RUNNABLE

3. Blocked State

  • A thread that is blocked waiting for a monitor lock.
  • Thread is temporarily inactive.
  • Waiting for a resource, lock, or another thread
  • Can occur during synchronization or I/O operations
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class BlockedStateDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread " + Thread.currentThread().getName() +
                        " State = " +   Thread.currentThread().getState() +
                        " holding lock");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread " + Thread.currentThread().getName() +
                        " State = " + Thread.currentThread().getState() + " acquired the lock"); 
            }

        });

        thread1.start();

        // Give thread1 a moment to acquire the lock
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        thread2.start();

        // Check thread2's state while it's waiting for the lock
        try {
            Thread.sleep(100);
            System.out.println("Thread " + thread2.getName() + 
                    " State = " + thread2.getState());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output:

Thread Thread-0 State = RUNNABLE holding lock
Thread Thread-1 State = BLOCKED
Thread Thread-1 State = RUNNABLE acquired the lock

4. Waiting State

  • A thread that is waiting indefinitely for another thread to perform a particular action
  • Entered when a thread calls Object.wait(), Thread.join(), or LockSupport.park()
  • Thread remains in this state until another thread calls notify() or notifyAll() on the monitor object
  • Different from BLOCKED state which is waiting to acquire a lock
  • Thread releases ownership of any monitors when entering this state
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class WaitingStateDemo {
    public static void main(String[] args) {
        final Object lock = new Object();

        Thread waiterThread = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("Waiter thread going to wait state");
                    lock.wait(); // Thread enters WAITING state
                    System.out.println("Waiter thread woke up");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread notifierThread = new Thread(() -> {
            try {
                // Sleep to give waiter thread time to enter waiting state
                Thread.sleep(1000);

                synchronized (lock) {
                    System.out.println("Notifier thread about to notify");
                    lock.notify(); // Wakes up waiter thread
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        waiterThread.start();

        // Check waiter thread state
        try {
            Thread.sleep(500); // Give waiter thread time to enter wait state
            System.out.println("Waiter Thread State: " + waiterThread.getState());

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

Output:

Waiter thread going to wait state
Waiter Thread State: WAITING
Notifier thread about to notify
Waiter thread woke up

5. Timed Waiting

  • Similar to WAITING state, but with a maximum time limit
  • A thread in this state is waiting for another thread to perform an action for up to a specified waiting time
  • Entered when a thread calls methods with timeout parameters such as Thread.sleep(millisecs), Object.wait(timeout), or Thread.join(timeout)
  • Thread automatically transitions to Runnable state after the specified timeout period elapses, or earlier if the specified condition is met
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TimedWaitingStateDemo {
    public static void main(String[] args) {
        Thread sleepingThread = new Thread(() -> {
            try {
                System.out.println("Thread going to sleep for 3 seconds");
                Thread.sleep(3000); // Thread enters TIMED_WAITING state
                System.out.println("Thread woke up after sleep");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        sleepingThread.start();

        // Check thread state while it's sleeping
        try {
            Thread.sleep(500); // Give thread time to enter sleep state
            System.out.println("Thread State: " + sleepingThread.getState());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output:

Thread going to sleep for 3 seconds
Thread State: TIMED_WAITING
Thread woke up after sleep

6. Terminated State

  • Thread has completed execution
  • Cannot be restarted
  • Resources are released
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class TerminatedStateDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is running");
            // Thread completes its execution and terminates
        });

        thread.start();

        // Wait for thread to complete execution
        try {
            thread.join(); // Wait for thread to terminate
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // Thread should now be in TERMINATED state
        System.out.println("Thread State: " + thread.getState());
    }
}

Output:

Thread is running
Thread State: TERMINATED

Thread Priority

Priority Levels

  • Java provides thread priorities from 1 to 10 with the default priority being 5
  • Thread.MIN_PRIORITY = 1
  • Thread.NORM_PRIORITY = 5 (default priority)
  • Thread.MAX_PRIORITY = 10

Setting and Getting Thread Priority

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ThreadPriorityDemo {
    public static void main(String[] args) {
        Thread lowPriorityThread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("Low Priority Thread: " + Thread.currentThread().getName());
            }
        });
        
        Thread highPriorityThread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println("High Priority Thread: " + Thread.currentThread().getName());
            }
        });
        
        // Set thread priorities
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
        highPriorityThread.setPriority(Thread.MAX_PRIORITY);
        
        // Get current thread priority
        System.out.println("Low Priority: " + lowPriorityThread.getPriority() + " for " + lowPriorityThread.getName());
        System.out.println("High Priority: " + highPriorityThread.getPriority() + " for " + highPriorityThread.getName());
        
        lowPriorityThread.start();
        highPriorityThread.start();
    }
}

Output:

Low Priority: 1 for Thread-0
High Priority: 10 for Thread-1
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
High Priority Thread: Thread-1
High Priority Thread: Thread-1
High Priority Thread: Thread-1
High Priority Thread: Thread-1
High Priority Thread: Thread-1
High Priority Thread: Thread-1
High Priority Thread: Thread-1
High Priority Thread: Thread-1
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
Low Priority Thread: Thread-0
High Priority Thread: Thread-1
High Priority Thread: Thread-1

Priority Inheritance and ThreadGroup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class PriorityInheritanceDemo {
    public static void main(String[] args) {
        // Current thread's priority affects child threads
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        
        ThreadGroup group = new ThreadGroup("PriorityGroup");
        group.setMaxPriority(7);
        
        Thread groupThread = new Thread(group, () -> {
            // Thread priority will be limited to group's max priority
            System.out.println("Group Thread Priority: " + Thread.currentThread().getPriority());
        });
        
        groupThread.start();
    }
}

Output:

Group Thread Priority: 7

Limitations of Thread Priorities

  • Thread priorities in Java are unreliable for precise scheduling control
  • OS dependency: Thread priorities map differently across Windows, Linux, and macOS
  • Limited control: Modern OS schedulers use complex algorithms beyond simple JVM priorities
  • No guarantees: Higher-priority threads aren’t guaranteed to run before lower-priority ones
  • Better alternatives exist in java.util.concurrent (thread pools, executors)
  • Next, we will use higher-level concurrency APIs instead of manipulating priorities directly