Mastering Java Concurrency: Threads, Synchronization, and Immutable Design
This article provides a step‑by‑step guide to Java concurrency, covering core concepts such as threads, runnables, thread lifecycle, synchronization primitives, wait/notify patterns, volatile variables, ThreadLocal storage, and how to design immutable objects for thread‑safety, all illustrated with concrete code examples and detailed explanations.
Introduction
Java is a powerful, general‑purpose language, and concurrency allows multiple threads to execute simultaneously, sharing resources and data. Using synchronized, the Lock interface, and other primitives, developers can avoid race conditions, data inconsistency, deadlocks, and other common pitfalls while improving performance and resource utilization.
Basic Concepts
Thread : an independent execution path inside a Java program, enabling parallel execution.
Runnable : a functional interface that encapsulates a task to be executed by a thread.
Synchronization : mechanisms such as synchronized blocks or methods that control access to critical code sections.
Locks and Mutexes : explicit constructs like ReentrantLock that manage access to shared resources.
Race Conditions : unpredictable behavior when multiple threads access shared data without proper coordination.
Data Race : a specific race condition where at least one thread modifies the shared data.
Deadlocks : a situation where two or more threads wait forever for resources held by each other.
Atomic Operations : thread‑safe operations provided by classes such as AtomicInteger and AtomicReference.
Thread‑Local Storage : per‑thread variables isolated from other threads.
Volatile : a keyword that guarantees visibility of variable updates across threads.
Java Memory Model (JMM) : defines how threads interact with memory and ensures happens‑before relationships.
Thread and Runnable
Creating a thread by extending Thread:
/**
* Test thread
*/
class TestThread extends Thread {
@Override
void run() {
for (int i = 1; i < 3; i++) {
println("Thread: ${Thread.currentThread().getName()} count: $i")
}
}
static void main(String[] args) {
TestThread t1 = new TestThread()
TestThread t2 = new TestThread()
t1.start()
t2.start()
println "Main thread ends"
}
}Console output demonstrates interleaved execution of the two threads.
Implementing Runnable separates the task from the thread object:
/**
* Test runnable
*/
class TestRunnable implements Runnable {
@Override
void run() {
for (int i = 1; i < 3; i++) {
println("Thread: ${Thread.currentThread().getName()} count: $i")
}
}
static void main(String[] args) {
TestRunnable r1 = new TestRunnable()
TestRunnable r2 = new TestRunnable()
Thread t1 = new Thread(r1)
Thread t2 = new Thread(r2)
t1.start()
t2.start()
println "Main thread ends"
}
}Thread Lifecycle and Methods
NEW: thread object created but not yet started. RUNNABLE: thread ready to run; JVM has allocated resources. BLOCKED: waiting to acquire a monitor lock. WAITING: waiting indefinitely for a condition. TIMED_WAITING: waiting with a timeout. TERMINATED: execution completed.
Key lifecycle methods: start(): transitions from NEW to RUNNABLE and invokes run(). wait(): releases the monitor and enters WAITING until notified. notify() / notifyAll(): wakes waiting threads. join(): blocks until the target thread finishes. yield(): hints to the JVM that the current thread is willing to pause. sleep(): pauses execution for a specified time. interrupt(): sets the interrupt flag and may cause InterruptedException.
Synchronized Keyword
The synchronized block acquires the monitor of a given object, ensuring exclusive access to the enclosed code. When the block exits, the lock is released, allowing other threads to acquire it.
class TestThread extends Thread {
static int count = 0;
static Object lock = new Object();
@Override
void run() {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
count++;
}
}
}
static void main(String[] args) throws Exception {
TestThread t1 = new TestThread();
TestThread t2 = new TestThread();
t1.start();
t2.start();
t1.join();
t2.join();
println "count = $count";
println "Main thread ends";
}
}Output shows count = 20, confirming that the synchronized block correctly protected the shared variable.
When applied to a method, the lock is the monitor of the instance (for non‑static methods) or the Class object (for static methods).
wait / notify / notifyAll
These methods are used together to build a condition‑based coordination pattern. The example below prints odd and even numbers alternately by having two threads wait on a shared lock and notify each other after each turn.
public class WaitNotifyExample {
private static final Object lock = new Object();
private static boolean isOddTurn = true;
public static void main(String[] args) {
Thread odd = new Thread(() -> {
for (int i = 1; i <= 10; i += 2) {
synchronized (lock) {
while (!isOddTurn) {
try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
System.out.println("Odd: " + i);
isOddTurn = false;
lock.notify();
}
}
});
Thread even = new Thread(() -> {
for (int i = 2; i <= 10; i += 2) {
synchronized (lock) {
while (isOddTurn) {
try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
System.out.println("Even: " + i);
isOddTurn = true;
lock.notify();
}
}
});
odd.start();
even.start();
}
}Key notes: always acquire the lock before calling wait or notify, and always check the condition inside a loop to guard against spurious wake‑ups.
volatile
Declaring a variable volatile forces every read and write to go directly to main memory, establishing a happens‑before relationship that guarantees visibility of the latest value across threads.
ThreadLocal Class
ThreadLocalprovides a separate instance of a variable for each thread, eliminating contention on shared mutable state. It is useful for per‑thread caches such as session data or database connections.
public class ThreadLocalExample {
/** Create a ThreadLocal with an initial value */
static ThreadLocal<Long> threadLocal = ThreadLocal.withInitial(() -> System.currentTimeMillis());
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(threadLocal.get());
}).start();
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
}Each thread prints a distinct timestamp, demonstrating isolation.
Immutable Objects
Immutable objects cannot change state after construction, making them inherently thread‑safe. The design steps include:
Declare the class final to prevent subclassing.
Mark all fields final and initialize them in the constructor.
Provide no setter methods.
Ensure safe publication (e.g., this does not escape during construction).
If a field references a mutable object, perform defensive copying and expose it only via unmodifiable wrappers.
Keep fields private.
When a modification is needed, return a new instance with the updated state.
final class ImmutablePerson {
private final String name;
private final int age;
private final List<ImmutablePerson> family;
ImmutablePerson(String name, int age, List<ImmutablePerson> family) {
this.name = name;
this.age = age;
List<ImmutablePerson> copy = new ArrayList<>(family);
this.family = Collections.unmodifiableList(copy);
}
String getName() { return name; }
int getAge() { return age; }
ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.family);
}
}Conclusion
The article walks through Java’s concurrency toolbox—from low‑level thread creation to high‑level immutability strategies—showing how each primitive works, when to use it, and how to combine them safely. By following the demonstrated patterns, developers can write correct, efficient, and maintainable multithreaded Java applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
