Understanding Java Threads: Core Concepts, Lifecycle, and Synchronization
This article explains the fundamentals of Java threading, covering process vs thread differences, context switching, daemon and user threads, thread states, synchronization mechanisms, interruption handling, and deadlock detection with concrete code examples and visual illustrations.
1. Background
Multithreading is the primary technique for implementing concurrent programming. A thread is an operating‑system concept. The article first defines processes and threads, explains their relationship, and highlights the differences.
1.1 Difference Between Process and Thread
Process
A process is an execution instance of a program and the basic unit of execution in the system. In Java, launching the main method starts a JVM process, and the thread that runs main is the main (user) thread. The following screenshot shows a macOS background process:
Thread
A thread is a smaller execution unit than a process. A process can contain multiple threads. Threads share the heap and method‑area of the process but have their own program counter, JVM stack, and native method stack, making thread creation and switching lighter than process switching; thus a thread is often called a lightweight process.
Windows users often install a computer‑guardian program, which runs as a single process with multiple threads handling virus scanning, junk cleaning, and system acceleration. This leads to the discussion of context switching .
1.2 What Is Context Switching?
During execution, a thread’s context (e.g., program counter, stack) may be saved and later restored when the thread is rescheduled. Context switching occurs in the following situations:
Voluntarily yielding the CPU, e.g., calling sleep() or wait().
Time slice exhaustion, preventing a thread from monopolizing the CPU.
Blocking on I/O or other system calls.
Thread termination.
The first three cases cause a thread switch, which requires saving the current context and loading the next thread’s context. Frequent context switches consume CPU and memory, reducing overall efficiency; therefore, multithreading does not always guarantee speed improvements.
Thread.Sleep(0) forces the OS to re‑contest the CPU, possibly keeping the current thread or handing control to another.
1.3 User Threads and Daemon Threads
Java distinguishes between daemon (background) threads and user threads . The JVM starts the main thread as a user thread; many daemon threads run in the background (e.g., garbage‑collection thread). When all user threads finish, the JVM exits regardless of daemon thread status.
public class DaemonDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " " +
(Thread.currentThread().isDaemon() ? "daemon thread" : "user thread"));
while (true) {
// busy loop
}
}, "t1");
// Set as daemon
t1.setDaemon(true);
t1.start();
// Pause 3 seconds then continue in main thread
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println("----------main thread finished");
}
}2. Thread Lifecycle and States
A Java thread can be in one of six states during its lifecycle:
NEW : Created but not started. start() moves it out of this state.
RUNNABLE : Ready to run or actually running after acquiring a time slice.
BLOCKED : Waiting for a monitor lock.
WAITING : Waiting indefinitely for another thread’s notification.
TIMED_WAITING : Waiting with a timeout (e.g., sleep(long), wait(long)).
TERMINATED : Execution has completed.
The state changes as code executes; the following diagram illustrates the transitions:
Creating a thread (e.g., new Thread()) puts it in NEW . Calling start() triggers native method start0(), moving the thread to READY and eventually to RUNNING when a time slice is allocated. Directly invoking run() executes the method in the current thread, not as a separate thread.
When a thread calls wait(), it enters WAITING and requires another thread to invoke notify() or notifyAll() to resume. TIMED_WAITING is entered via sleep(long) or timed wait(long), after which the thread returns to RUNNABLE .
Entering a synchronized block or method while the lock is held by another thread leads to the BLOCKED state.
Thread Waiting and Wake‑up Mechanisms
Using Object.wait() / Object.notify() inside a synchronized block.
public static void main(String[] args) {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); }
}
System.out.println(Thread.currentThread().getName() + "\t" + "woken up");
}, "t1").start();
// pause
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
synchronized (lock) { lock.notify(); }
}, "t2").start();
}Using Condition.await() / Condition.signal() with a ReentrantLock.
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t" + "start");
condition.await();
System.out.println(Thread.currentThread().getName() + "\t" + "woken up");
} catch (InterruptedException e) { e.printStackTrace(); }
finally { lock.unlock(); }
}, "t1").start();
// pause
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
lock.lock();
try { condition.signal(); }
finally { lock.unlock(); }
System.out.println(Thread.currentThread().getName() + "\t" + "signaled");
}, "t2").start();
}Using LockSupport.park() / LockSupport.unpark().
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis() + "---unparked");
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t" + System.currentTimeMillis() + "---unpark over");
}How to Interrupt a Thread
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("-----t1 thread interrupted, breaking, ending");
break;
}
System.out.println("-----hello");
}
}, "t1");
t1.start();
System.out.println("**************" + t1.isInterrupted());
try { TimeUnit.MILLISECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
t1.interrupt();
System.out.println("**************" + t1.isInterrupted());
}Calling interrupt() sets the thread’s interrupt flag to true. If the thread is in a blocking state (e.g., sleep, wait, join), it immediately exits the block and throws InterruptedException. Otherwise, the thread continues running and must periodically check the flag.
3. Thread Deadlock
A deadlock occurs when two or more threads wait for each other’s resources, causing a standstill. The necessary conditions are mutual exclusion, hold‑and‑wait, no preemption, and circular wait.
Typical resolution strategies include terminating all deadlocked processes, aborting them one by one, forcibly pre‑empting resources, or reallocating resources from other processes.
Example code demonstrating a classic deadlock:
public static void main(String[] args) {
final Object lockA = new Object();
final Object lockB = new Object();
new Thread(() -> {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "holds A, wants B");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t" + "acquired B");
}
}
}, "A").start();
new Thread(() -> {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t" + "holds B, wants A");
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t" + "acquired A");
}
}
}, "B").start();
}Running jps -l to obtain the process ID and then jstack <pid> reveals the deadlock details, showing each thread waiting for the other's monitor.
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.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
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.
