Fundamentals 16 min read

Concurrency 07: Understanding Deadlocks and Thread‑Safety Strategies

This article explains how deadlocks arise from four necessary conditions, shows how to detect them with jstack, outlines practical ways to prevent deadlock, distinguishes livelock and starvation, and presents three prioritized approaches—immutability, thread confinement, and synchronization—to achieve thread safety in Java.

Dabaoshi
Dabaoshi
Dabaoshi
Concurrency 07: Understanding Deadlocks and Thread‑Safety Strategies

Deadlock example

A naive transfer implementation locks the source account then the destination account:

void transfer(Account from, Account to, int amount) {
    synchronized (from) {          // ① lock source account
        synchronized (to) {       // ② lock destination account
            from.balance -= amount;
            to.balance   += amount;
        }
    }
}

If two threads perform opposite transfers simultaneously, each holds one account lock and waits for the other, forming a circular wait and causing the program to hang without errors or high CPU usage.

Four necessary conditions for deadlock

Mutual Exclusion : a resource can be held by only one thread at a time (e.g., a synchronized lock).

Hold and Wait : a thread holding at least one resource requests another without releasing the first.

No Preemption : a held resource cannot be forcibly taken away; it must be released voluntarily.

Circular Wait : a closed chain of threads each waiting for a resource held by the next thread.

Breaking any one of these conditions prevents deadlock.

Detecting deadlock with jstack

Three‑step workflow:

Identify the Java process PID using jps -l.

jps -l
# 12345 com.example.TransferApp   ← note this PID

Run jstack <PID> to dump thread stacks; the tool automatically detects deadlocks. jstack 12345 Examine the “Found one Java‑level deadlock” section, which lists which thread is waiting for which lock and which thread holds it.

Found one Java-level deadlock:
=============================
"Thread-A": waiting to lock monitor 0x... (object 0x...account2...), which is held by "Thread-B"
"Thread-B": waiting to lock monitor 0x... (object 0x...account1...), which is held by "Thread-A"

Other tools such as jconsole, VisualVM, and Arthas thread -b provide similar detection capabilities.

Preventing deadlock by breaking a condition

Break Circular Wait – ordered locking :

void transfer(Account from, Account to, int amount) {
    // lock accounts in a fixed order to avoid circular wait
    Account first  = from.id < to.id ? from : to;
    Account second = from.id < to.id ? to   : from;
    synchronized (first) {
        synchronized (second) {
            from.balance -= amount;
            to.balance   += amount;
        }
    }
}

Break Hold and Wait – batch lock : acquire all required locks atomically (rarely used in practice but conceptually important).

Break No Preemption – tryLock with timeout :

if (lock1.tryLock(1, SECONDS)) {
    try {
        if (lock2.tryLock(1, SECONDS)) {
            try {
                // transfer logic
            } finally { lock2.unlock(); }
        }
        // could not get second lock → release first and retry
    } finally { lock1.unlock(); }
}

The preferred prevention method is consistent lock ordering; ReentrantLock.tryLock serves as a fallback when ordering is insufficient.

Livelock and starvation

Livelock : threads keep running but make no progress, often due to symmetric back‑off. Typical remedy is to introduce randomness in retry timing.

Starvation : some threads never acquire the needed resource, commonly caused by non‑fair locks. Using a fair (FIFO) lock resolves it at the cost of throughput.

Thread‑safety strategies

Three routes to achieve thread safety:

Immutable objects : state never changes after construction. Example:

public final class Money {
    private final long amount;
    private final String currency;
    public Money(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    public long getAmount() { return amount; }
    public Money plus(long delta) { return new Money(amount + delta, currency); }
}

Thread confinement : avoid sharing data. Forms include stack confinement (local variables), ThreadLocal variables, and per‑thread resources such as dedicated database connections.

Synchronization : when data must be shared and mutable, use synchronized, Lock, atomic classes, or concurrent collections. This is the most general but also the most error‑prone path.

Priority order: Immutable > Thread Confinement > Synchronization . Ask “Can it be immutable? If not, can it be confined? Only then consider synchronization.”

Safe publication

Even a correctly constructed immutable object can be observed in a partially‑initialized state if published incorrectly. Proper publication techniques include:

Static initialization.

Writing the reference to a volatile field.

Using the same lock for both write and read.

Passing the reference through a concurrent container or via thread start, which provides a happens‑before guarantee.

Declaring a field final only prevents reassignment; it does not establish cross‑thread visibility. Correct publication ensures that other threads see the fully constructed object.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyDeadlockThread SafetyImmutablejstackLock OrderingLivelock
Dabaoshi
Written by

Dabaoshi

Practical utilities

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.