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:
- What is the difference between concurrency and parallelism?
- How do threads differ from processes?
- What are the different ways to create threads in Java?
- How do thread states work and what are their lifecycle transitions?
- 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.
- Extending Thread Class: Inherit from the base
Thread
class and override therun()
method. - Implementing Runnable Interface: Implement the
Runnable
interface and override therun()
method. - Anonymous Inner Class: Create threads using anonymous inner classes.
- 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.
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.
|
|
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.
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.
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:
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:
|
|
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
Method | Description | Example |
---|---|---|
start() | Begins thread execution, calls the run() method | thread.start(); |
run() | Contains code to be executed in the thread | Thread thread = new Thread(() -> { /* code */ }); |
sleep(long millis) | Pauses thread execution for specified milliseconds | Thread.sleep(1000); // sleep 1 second |
join() | Waits for thread to die | thread.join(); // wait for thread completion |
interrupt() | Interrupts a thread in sleep/wait state | thread.interrupt(); |
isAlive() | Tests if thread is still running | if(thread.isAlive()) { /* code */ } |
getName() | Returns this thread’s name | String name = thread.getName(); |
setName(String name) | Sets this thread’s name | thread.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 state | Thread.State state = thread.getState(); |
currentThread() | Returns a reference to the currently executing thread | Thread 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:
State | Description | Methods |
---|---|---|
New | A thread that has not yet started. | Thread constructor new Thread() |
Runnable | A thread executing in the Java virtual machine. | thread.start() |
Blocked | A thread that is blocked waiting for a monitor lock. | Attempting to enter a synchronized block/method |
Waiting | A thread that is waiting indefinitely for another thread to perform a particular action. | Thread.join() , Object.wait() |
Timed Waiting | A 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) |
Terminated | A 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
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.
|
|
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
|
|
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()
, orLockSupport.park()
- Thread remains in this state until another thread calls
notify()
ornotifyAll()
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
|
|
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)
, orThread.join(timeout)
- Thread automatically transitions to Runnable state after the specified timeout period elapses, or earlier if the specified condition is met
|
|
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
|
|
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
= 1Thread.NORM_PRIORITY
= 5 (default priority)Thread.MAX_PRIORITY
= 10
Setting and Getting Thread Priority
|
|
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
|
|
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