Prefer Runnable or ExecutorService over extending Thread for most application code. Separating the task from the thread improves design.
public class ThreadDemo {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
};
Thread thread = new Thread(task, "worker-1");
thread.start();
}
}
A race condition happens when the result depends on timing between threads. synchronized protects critical sections by allowing one thread at a time.
class Counter {
private int value;
synchronized void increment() {
value++;
}
int getValue() {
return value;
}
}
ExecutorService manages worker threads for you. It is better than manually creating many Thread objects.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.shutdown();
}
}
Java provides volatile, locks, atomic classes, concurrent collections, and higher-level utilities. Prefer higher-level utilities when possible.
A race is not limited to two threads incrementing the same number. One thread may observe stale fields or an object before construction is safely published. synchronized, volatile, final-field guarantees, locks, concurrent collections, thread start, and thread join create specific happens-before relationships that make writes visible in a defined order.
volatile is suitable for visibility of an independent state value, but it does not turn a read-modify-write sequence into one atomic action. Use an atomic type, a lock, or confinement when multiple fields must change together. Prefer immutable messages and task ownership so fewer invariants depend on shared mutation.
Interruption is a cooperative cancellation signal. Blocking methods may throw InterruptedException, and code that cannot complete the cancellation should restore the interrupt status rather than swallowing it. A task must also release locks, files, sockets, and partial state when cancellation occurs.
ExecutorService separates task submission from thread creation, but its queue size, rejection policy, thread count, and shutdown behavior are part of the application contract. Bound queues when overload must be visible. Stop accepting work, request shutdown, wait for a defined period, and escalate deliberately if tasks do not finish. Capture thread dumps when progress stops so deadlock, starvation, and blocked I/O can be distinguished.
A process is an independent program with its own memory space. A thread is a lightweight unit within a process that shares memory with other threads. Threads are faster to create and communicate, but require synchronization.
A synchronized method locks the entire object (this). A synchronized block locks only a specific object for a specific section of code - more granular and better for performance.
Deadlock occurs when Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Prevent it by: always acquiring locks in the same order, using tryLock() with timeout, or using higher-level concurrency utilities.
Explore 500+ free tutorials across 20+ languages and frameworks.