Introduction to Creating Threads in Java

Java threads let a single program do many things at once without waiting for each task to finish before starting the next. This ability is essential for responsive desktop tools, fast web servers, and background services that must stay lively while they work.

Creating a thread is only the first step; understanding how to start it, stop it cleanly, and coordinate it with others decides whether your application feels smooth or fragile. The code samples below stay short, compile on any modern JDK, and avoid external libraries so you can paste them straight into an empty main method and watch the concepts run.

Core Concepts Behind Java Threads

A thread is a lightweight unit of execution that shares memory with its parent process yet runs its own call stack. The JVM maps Java threads to native OS threads, so switching between them is fast and handled by the operating system scheduler.

Every Java program starts with one thread called the main thread; you can add more by either extending java.lang.Thread or implementing java.lang.Runnable. The choice shapes how you organize code and how easily you can test or reuse it later.

Process vs Thread

A process owns separate memory, file handles, and security context, while threads inside the same process read the same objects and static fields. This shared space makes communication cheap but demands discipline to avoid data races.

Concurrency vs Parallelism

Concurrency means tasks overlap in time; parallelism means they literally run on multiple CPU cores at once. Java supports both, but you must design for concurrency even on a single core because thread scheduling is non-deterministic.

Two Canonical Ways to Create a Thread

Java offers two direct mechanisms: subclass Thread or pass a Runnable to a Thread constructor. Both end up invoking Thread.start(), which registers the new call stack with the JVM and returns immediately.

Extending Thread

Subclassing Thread is the shortest path to a runnable demo. Override run() to contain the work, instantiate the class, and call start().

“`java
class PingThread extends Thread {
public void run() {
for (int i = 0; i < 5; i++) { System.out.println("ping"); try { Thread.sleep(500); } catch (InterruptedException e) { return; } } } } public class DemoExtend { public static void main(String[] args) { new PingThread().start(); System.out.println("main keeps going"); } } ```

The subclass couples task logic with thread mechanics, which can limit reuse if you later want to submit the same job to an executor.

Implementing Runnable

Implementing Runnable separates the task from the thread that runs it. You can submit the same instance to many threads or to a thread pool without copying code.

“`java
class CounterJob implements Runnable {
public void run() {
for (int i = 1; i <= 3; i++) { System.out.println(Thread.currentThread().getName() + " count " + i); } } } public class DemoRunnable { public static void main(String[] args) { Runnable job = new CounterJob(); new Thread(job, "A").start(); new Thread(job, "B").start(); } } ```

Because Runnable is a functional interface, you can also write the body as a lambda: new Thread(() -> System.out.println(“lambda”)).start();

Starting, Sleeping, and Interrupting

Call start() once; calling it again throws IllegalThreadStateException. The JVM allocates a native thread and invokes your run() method asynchronously.

Thread.sleep(long ms) pauses the current thread without consuming CPU, but it can throw InterruptedException when another thread calls interrupt(). Treat interruption as a polite request to finish early rather than a fatal error.

“`java
public class SleepExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
System.out.print(“.”);
Thread.sleep(1000);
}
} catch (InterruptedException ok) {
System.out.println(“interrupted while sleeping”);
}
});
worker.start();
Thread.sleep(3500);
worker.interrupt();
}
}
“`

Thread Lifecycle States

A thread can be NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, or TERMINATED. These are JVM view states, not OS kernel states, and you can inspect them with getState().

NEW means you created the object but have not called start(). Once start() returns, the thread enters RUNNABLE, which includes both running on a core and ready to run but waiting for scheduler time.

BLOCKED occurs when a thread tries to enter a synchronized section already held by another thread. WAITING and TIMED_WAITING show up when the code calls Object.wait, Thread.join, LockSupport.park, or sleep with or without a timeout.

Visualizing States

“`java
Thread t = new Thread(() -> LockSupport.park());
System.out.println(t.getState()); // NEW
t.start();
Thread.sleep(50);
System.out.println(t.getState()); // WAITING
LockSupport.unpark(t);
t.join();
System.out.println(t.getState()); // TERMINATED
“`

Use these states during debugging, but never write business logic that depends on them because they can change between instructions.

Coordinating Multiple Threads

When threads share data, access must be serialized or otherwise controlled to prevent inconsistent reads and writes. Java offers synchronized blocks, explicit locks, atomic variables, and concurrent collections.

Synchronized Blocks

Marking a method synchronized locks on the instance or Class object; using synchronized(this) lets you narrow the guarded section to only the critical lines.

“`java
class SafeCounter {
private int count = 0;
public void increment() {
synchronized (this) { count++; }
}
public int get() { return count; }
}
“`

Locking on this is simple but exposes the lock to external callers who might accidentally deadlock by acquiring it in unexpected order.

Volatile for Visibility

Declaring a field volatile guarantees that every read sees the last write, but it does not provide mutual exclusion. Use volatile for status flags that one thread writes and others read.

“`java
class ShutdownFlag {
private volatile boolean running = true;
public void shutdown() { running = false; }
public void work() {
while (running) {
// do something
}
}
}
“`

Common Pitfalls and Simple Fixes

Deadlock happens when two threads each hold a lock the other needs. Acquire locks in a fixed global order or use tryLock with a timeout to break the cycle.

Spurious wakeups can make wait() return even when no notify() occurred; always call wait() inside a while loop that rechecks the condition. This pattern also protects against race windows where another thread sneaks in and changes state.

“`java
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
process(queue.remove());
}
“`

Thread Starvation

A thread that never gets CPU time is starved, often because higher-priority threads monopolize cores. Keep priority tweaking minimal; most programs should rely on fair queuing in executors rather than raw priorities.

Using the Concurrency Utilities

Modern Java favors java.util.concurrent over raw Thread for most work. Executors turn threads into a managed resource pool, and concurrent collections hide complex locking inside well-tested classes.

ExecutorService

Submitting tasks to an ExecutorService separates task submission from execution policy, letting you change pool size or queue type without touching business code.

“`java
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> System.out.println(“running in pool”));
pool.shutdown();
“`

Always call shutdown() when the application exits; otherwise the JVM keeps running because the worker threads are non-daemon.

Callable and Future

When you need a result back, submit a Callable and receive a Future. The get() method blocks until the computation completes or throws an exception wrapped in ExecutionException.

“`java
ExecutorService pool = Executors.newSingleThreadExecutor();
Future f = pool.submit(() -> 2 + 3);
System.out.println(f.get()); // 5
pool.shutdown();
“`

Testing Threaded Code

Unit tests that sleep for fixed delays are flaky under load. Instead, use CountDownLatch or CyclicBarrier to let threads proceed only when the test driver releases them, ensuring deterministic ordering.

Stress testing tools like jcstress or simply running many iterations on a multi-core machine can surface races that appear only under contention. Keep tests short but numerous to maximize CPU shuffle.

“`java
CountDownLatch start = new CountDownLatch(1);
Runnable task = () -> {
try { start.await(); } catch (InterruptedException e) { return; }
// exercise code
};
new Thread(task).start();
start.countDown(); // unleash all threads at once
“`

Graceful Shutdown Patterns

Never call Thread.stop(); it can leave objects in half-updated states. Instead, use interruption, poison pills, or executor shutdown to let threads finish current work and exit cleanly.

A poison pill is a special object placed on a shared queue that signals consumers to return. When each consumer sees the pill, it stops polling and dies, allowing the main thread to join() and exit.

“`java
class Consumer implements Runnable {
private final BlockingQueue q;
public void run() {
try {
String item;
while (!(item = q.take()).equals(“POISON”)) {
process(item);
}
} catch (InterruptedException ok) {
Thread.currentThread().interrupt();
}
}
}
“`

Choosing Between Virtual and Platform Threads

Java 21 introduces virtual threads that are cheap to create and block without pinning OS threads. They suit workloads with many blocking calls, such as simple web handlers or chat bots.

Creating a virtual thread looks identical to creating a platform thread except you use Thread.ofVirtual().factory(). Virtual threads still obey the same interruption and synchronization rules, so existing Runnable code migrates without change.

Use platform threads for CPU-bound computations that truly need dedicated cores; use virtual threads when concurrency count is high but each task spends most time waiting on I/O.

Key Takeaways for Everyday Coding

Favor immutability to avoid locks altogether; an object that never changes after construction is automatically thread-safe. When mutation is unavoidable, isolate it inside well-reviewed concurrent utilities rather than hand-rolling synchronized blocks.

Log thread names generously; when production logs interleave, knowing which line came from which worker saves hours of guesswork. Keep the threading model explicit in code reviews so every team member understands where shared state lives and who guards it.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *