Difficulty: Introductory to Intermediate | Prerequisites: Basic Java syntax, classes, methods, loops, and exception handling.
Most programs you have written so far do one thing at a time: execute line A, then line B, then line C. Multithreading breaks that constraint. It lets a single Java program run several tasks concurrently, each on its own thread of execution. This unit uses a simple clock application to show why that matters and how Java's Thread class, the Runnable interface, thread priorities, and the volatile keyword work together. If you are comfortable writing basic Java classes and handling exceptions, you have the background you need.
A Java thread is a separate path of execution. The clock app creates two threads: one that fetches the current time every second, and one that prints it to the console. Thread priorities tell the scheduler which thread deserves more CPU attention, and the volatile keyword makes sure both threads always see the same, up-to-date value of a shared variable.
Thread
A thread is a single, sequential flow of control within a program. Every Java program starts with at least one thread (the main thread). Additional threads let the program do more than one thing at the same time.
Think of it as: a separate worker inside your program, each following its own set of instructions simultaneously.
Multithreading
The ability of a program (or an operating system) to run multiple threads concurrently. Java supports multithreading natively through the java.lang.Thread class and the Runnable interface.
In simple terms, this means your program can juggle several tasks at once rather than finishing one before starting the next.
Runnable (interface)
A functional interface in java.lang with a single method, run(). A class that implements Runnable defines the work a thread will perform. You pass an instance of that class to a Thread object, which then executes run() on a new thread.
Think of it as: a job description. The Thread is the worker; the Runnable is the task sheet you hand them.
Thread.sleep(long millis)
A static method that pauses the current thread for the specified number of milliseconds. It throws InterruptedException if another thread interrupts the sleeping thread.
In simple terms, this means "do nothing for this many milliseconds, then carry on."
volatile (keyword)
A Java keyword applied to a field to guarantee that every read of that field comes from main memory (not a thread-local cache). It ensures visibility: when one thread writes to a volatile variable, every other thread immediately sees the new value.
Think of it as: a shared whiteboard. Instead of each worker keeping a private notepad, everyone reads and writes on the same board.
Thread priority
An integer hint (1 to 10) that tells the Java thread scheduler how much CPU time a thread should receive relative to other threads. 1 is MIN_PRIORITY, 5 is NORM_PRIORITY (the default), and 10 is MAX_PRIORITY. Priorities are suggestions, not guarantees; the actual scheduling depends on the operating system.
Shared variable
A variable that is accessible to more than one thread. Without proper synchronisation (e.g. volatile or synchronized), threads may read stale values from their own cached copies.
InterruptedException
A checked exception thrown when a thread that is sleeping, waiting, or otherwise blocked is interrupted by another thread calling its interrupt() method. Handling it lets the program shut down threads cleanly.
Graceful shutdown
A pattern where threads are stopped in a controlled way, typically by setting a boolean flag (running = false) so that each thread's loop exits on its next iteration, rather than forcibly killing the thread.
In simple terms, this means asking each thread to finish what it is doing and stop, rather than pulling the plug.
Java offers two main ways to define a thread's work: extending the Thread class, or implementing the Runnable interface.
The clock app uses Runnable. The class declares implements Runnable and provides a public void run() method containing the thread's logic.
A Thread object wraps the Runnable and starts execution when you call start(). Calling start() (not run()) is what actually launches a new thread; calling run() directly would execute the method on the current thread, defeating the purpose.
New: the Thread object has been created but start() has not been called.
Runnable: start() has been called; the thread is eligible for CPU time.
Timed waiting: the thread has called Thread.sleep() and is paused for a set duration.
Terminated: run() has returned, or the thread was interrupted and its loop exited.
You can pass a name string as the second argument to the Thread constructor, e.g. new Thread(clock, "Clock-Updater-Thread").
Named threads make debugging and logging easier because stack traces and print statements identify which thread produced them.
The application is split across three classes, each with a clear role.
Responsible for fetching the current date and time.
Uses LocalDateTime.now() and a DateTimeFormatter with the pattern "HH:mm:ss dd-MM-yyyy".
Stores the formatted string in a private String currentTime field.
Its run() loop: get the time, format it, store it, then Thread.sleep(1000) (pause one second), repeat.
Exposes getCurrentTime() so other objects can read the latest value.
Provides stopClock(), which sets running = false to end the loop gracefully.
Responsible for printing the time to the console.
Receives a Clock reference through its constructor, so it can call clock.getCurrentTime().
Its run() loop: read the time from Clock, print it if non-null, then Thread.sleep(1000), repeat.
Provides stopDisplay() to end its own loop.
Creates one Clock instance and one ClockDisplay instance.
Wraps each in a Thread, naming them "Clock-Updater-Thread" and "Clock-Display-Thread".
Sets priorities: updater at 5 (normal), display at 10 (maximum).
Calls start() on both threads.
The main thread sleeps for 15 seconds (Thread.sleep(15000)), then calls stopClock() and stopDisplay() to shut everything down.
setPriority(int) accepts values 1 through 10.
The clock app sets the updater thread to 5 and the display thread to 10. The intent is that the display thread gets scheduled more often, so the user sees smooth updates.
Priorities are advisory. The JVM delegates actual scheduling to the operating system, so behaviour varies across platforms. You should never write code that depends on priority for correctness; use priorities only to suggest relative importance.
Both threads access currentTime. The updater writes it; the display reads it.
Without any coordination, the JVM is free to cache a field's value inside each thread's working memory. The display thread might read a stale copy and print outdated or null time strings.
Marking a field volatile forces every read and write to go through main memory.
This gives you visibility: when the updater thread writes a new time string, the display thread's next read is guaranteed to see it.
volatile does not provide atomicity for compound operations (e.g. check-then-act). For a simple clock where one thread writes a reference and another reads it, volatile is sufficient. For more complex shared state, you would need synchronized blocks or classes from java.util.concurrent.
The assignment's Clock class declares currentTime as a plain private String without the volatile keyword. The written explanation describes volatile as part of the design. In an exam or interview setting, you should know that volatile is the correct choice here, and that omitting it is a latent bug: the display thread could, in theory, cache a stale reference.
public class Clock implements Runnable {
@Override
public void run() {
while (running) {
// thread's repeating work goes here
}
}
}The class promises to provide a run() method. The while (running) loop is the standard idiom for a long-lived thread that should stop when told.
Thread updaterThread = new Thread(clock, "Clock-Updater-Thread");
updaterThread.setPriority(5);
updaterThread.start();Three steps: wrap the Runnable in a Thread, optionally set priority and name, then call start(). The JVM calls run() on a new thread behind the scenes.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Clock thread stopped: " + e.getMessage());
running = false;
}Thread.sleep(1000) pauses for one second. If something interrupts the thread during that sleep, the catch block sets running = false so the loop exits cleanly.
public void stopClock() {
running = false;
}The main method calls this after 15 seconds. On the next loop iteration the while (running) check fails, and run() returns, which terminates the thread.
Digital clocks and timers: any on-screen clock updates its display on one thread while keeping time on another, exactly as this app does.
Web servers: each incoming HTTP request is typically handled on its own thread so the server can serve many users at once.
GUI applications (Swing, JavaFX): a background thread fetches data or does computation while the UI thread stays responsive to clicks and keystrokes. Blocking the UI thread freezes the interface.
Games: rendering, physics, audio, and networking often run on separate threads so that a slow network response does not stall the frame rate.
File downloads: a progress bar updates on the UI thread while the download runs on a background thread.
"Calling run() starts a new thread." It does not. Calling run() directly executes the method on the calling thread, just like any other method call. You must call start() to get a new thread of execution.
"Higher priority means a thread always runs first." Thread priorities are hints to the scheduler, not hard rules. The OS may ignore them entirely. Code correctness must never depend on priority.
"Two threads reading the same variable will always see the latest value." Without volatile or synchronized, the JVM is permitted to cache field values per-thread. One thread can write a new value that another thread never sees.
"Thread.sleep(1000) guarantees exactly one second." sleep() guarantees at least the specified time, but the actual delay may be longer depending on system load and scheduling granularity.
Know the difference between start() and run(). This is one of the most commonly tested points on Java threading.
Be able to explain what volatile does and when it is (and is not) sufficient. A typical exam question: "What happens if volatile is removed from a shared variable accessed by two threads?"
Understand the Runnable interface vs. extending Thread. Know why Runnable is generally preferred (a class can implement multiple interfaces but extend only one class).
Thread priority range (1 to 10), the three named constants (MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY), and the fact that priorities are not guarantees.
Graceful shutdown: the boolean-flag pattern for stopping threads. Contrast with Thread.stop(), which is deprecated because it can leave shared data in an inconsistent state.
InterruptedException: know that sleep(), wait(), and join() can throw it, and that catching it is where you clean up.
True or False: Calling myThread.run() launches a new thread of execution. (False. You must call start().)
Fill in the blank: Java thread priorities range from ___ (lowest) to ___ (highest). (1 to 10.)
True or False: The volatile keyword makes a variable's operations atomic. (False. It guarantees visibility, not atomicity.)
Fill in the blank: The standard way to stop a looping thread gracefully is to set a _______ flag to false. (boolean / running)
True or False: Thread.sleep(1000) always pauses for exactly 1,000 milliseconds. (False. It guarantees at least 1,000 ms; the actual pause may be slightly longer.)
Q: What is the difference between calling start() and calling run() on a Thread object?
A: start() creates a new thread of execution and then invokes run() on that new thread. Calling run() directly executes the method on the current thread, so no new thread is created.
Q: In the clock application, why is the display thread given a higher priority than the updater thread?
A: The display thread is given priority 10 (MAX_PRIORITY) so that the thread scheduler favours it when allocating CPU time. This makes the console output update more smoothly and responsively. The updater thread runs at normal priority (5) because its work, fetching the time, is less time-sensitive.
Q: Explain what the volatile keyword does and why it would be important for the currentTime variable in the Clock class.
A: volatile ensures that when one thread writes to the variable, the new value is immediately visible to all other threads. Without it, each thread may cache its own copy of currentTime, and the display thread could read a stale (outdated) value. For a shared variable written by one thread and read by another, volatile prevents this visibility problem.
Q: What is a race condition, and does the clock application have one?
A: A race condition occurs when two or more threads access shared data concurrently and the outcome depends on the order of execution. In this application, the updater writes currentTime and the display reads it. Because the variable is a simple reference (a String), and one thread only writes while the other only reads, volatile alone is sufficient to avoid a harmful race. If multiple threads were both reading and writing, or performing compound operations, synchronized blocks would be needed.
Q: Why is Thread.stop() deprecated, and what does the clock application use instead?
A: Thread.stop() forcibly terminates a thread, which can leave shared objects in an inconsistent state (e.g. partially updated data). The clock application uses a boolean flag (running). Setting it to false lets the thread finish its current loop iteration and exit run() naturally, ensuring a clean shutdown.
Q: What would happen if both threads were given the same priority?
A: They would both be treated equally by the scheduler. On most systems this means they take turns (time-slicing). The application would still work, but the display thread would not be favoured over the updater thread, so there could be marginally less smooth console output.
This material connects directly to concurrency and synchronisation, which are covered in more depth when you study synchronized methods and blocks, locks, and the java.util.concurrent package. Understanding threads here makes those topics far easier to pick up.
It also connects to GUI programming (Swing, JavaFX), where the event-dispatch thread must never be blocked by long-running work. The pattern of "background thread does computation, UI thread updates the screen" is exactly what the clock app demonstrates at a smaller scale.
More broadly, multithreading is foundational to operating systems (process vs. thread, scheduling algorithms, deadlocks) and distributed systems (handling concurrent requests across networked services).
Java threads, multithreading, Runnable interface, Thread class, thread priority, setPriority, MIN_PRIORITY, NORM_PRIORITY, MAX_PRIORITY, volatile keyword, shared variable, thread safety, visibility, Thread.sleep, InterruptedException, graceful shutdown, boolean flag, concurrent programming, concurrency, Java scheduler, time-slicing, clock application, background thread, foreground thread, run() vs start(), Thread.stop() deprecated, java.lang.Thread, java.util.concurrent, synchronized, race condition, thread-local cache, main memory