Java Multithreading
Java multithreading means running more than one thread of execution inside the same program. A thread is a separate path of work, so a program can keep doing one task while another task is also running.
What Threads Are For
Threads are useful when a program has work that can happen independently, such as handling several client requests, keeping a user interface responsive, or doing background calculations. Java programs always start with at least one thread: the main thread that runs main.
You can create another thread by passing a Runnable to a Thread object. The Runnable contains the code that the new thread should run. Calling start begins the new thread. Do not call run directly when you want a new thread, because that just runs the method on the current thread.
Starting and Joining a Thread
The join method makes one thread wait until another thread has finished. This is important when the main thread needs a result or must avoid ending too early.
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker thread is running");
System.out.println("Worker thread is done");
});
System.out.println("Main thread starts worker");
worker.start();
worker.join();
System.out.println("Main thread continues");
}
}
Output:
Main thread starts worker
Worker thread is running
Worker thread is done
Main thread continues
How the Example Works
The program creates a Thread and gives it a lambda expression. That lambda is the Runnable task. After worker.start(), Java may run the worker thread at the same time as the main thread. The call to worker.join() tells the main thread to wait until the worker finishes, so the final line prints last.
Shared Data and Race Conditions
Threads often need to share objects. This is where mistakes can happen. A race condition occurs when the result depends on the exact timing of multiple threads. For example, two threads updating the same number at the same time can lose updates if the operation is not protected.
One common way to protect shared state is the synchronized keyword. A synchronized instance method lets only one thread at a time run that method on the same object.
public class Main {
static class Counter {
private int value = 0;
public synchronized void increment() {
value++;
}
public int getValue() {
return value;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread first = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread second = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
first.start();
second.start();
first.join();
second.join();
System.out.println("Final count: " + counter.getValue());
}
}
Output:
Final count: 2000
Why synchronized Matters
The expression value++ looks like one action, but it involves reading the current value, adding one, and storing the new value. Without synchronization, two threads could read the same old value and both store the same new value. With synchronized, only one thread can execute increment on the shared Counter object at a time.
Important Thread Methods
| Method | Purpose |
|---|---|
start() |
Starts a new thread and calls its task |
join() |
Waits for a thread to finish |
sleep(milliseconds) |
Pauses the current thread for a short time |
isAlive() |
Checks whether a thread is still running |
Good Habits
- Keep shared mutable data as small as possible.
- Use
joinwhen the main thread must wait for worker threads. - Protect shared updates with
synchronizedor higher-level concurrency tools. - Do not rely on thread output order unless your code explicitly controls it.
Takeaway: Java threads let work run concurrently, but shared data must be protected so the program gives correct results every time.
