CodeToConcept
About

java

java Tutorials

Introduction
Java Variables
Java Data Types
Java Type Casting
Java Operators
Java Input/Output
Java Class & Object
java Method
Java Constructor
Java this keyword
Java super keyword
Java Method Overloading and Overriding
Java Interface
Java Abstract Class
Java Memory Management
Design Patterns
JDBC (Database Connectivity)
Java IDEs

java Tutorials

Introduction
Java Variables
Java Data Types
Java Type Casting
Java Operators
Java Input/Output
Java Class & Object
java Method
Java Constructor
Java this keyword
Java super keyword
Java Method Overloading and Overriding
Java Interface
Java Abstract Class
Java Memory Management
Design Patterns
JDBC (Database Connectivity)
Java IDEs

Sample Question

Think it through, then check the tutorial.

CodeToConcept

Quickly learn through simple, structured, and practical coding tutorials. Build your confidence from step zero to real-world experience.

© 2026 CodeToConcept. All rights reserved.

Quick Links

AboutTutorials

Tutorials

JavaHTMLCSS

Support

Privacy PolicyTerms of ServiceContact
GitHubTwitterYouTube

Deadlock in Java

A deadlock leaves work stuck because each participant is waiting for something another blocked participant holds. In Java, two threads acquiring two locks in opposite orders are a common demonstration. We will examine that dependency and how to avoid creating it.

What is Deadlock?

A deadlock occurs when two or more threads are permanently waiting for resources or locks held by each other.

For example:

Thread-1
   |
   | holds Lock-A
   |
   +------> waiting for Lock-B
                ^
                |
             holds Lock-B
                |
             Thread-2
                |
                +------> waiting for Lock-A

Neither thread can continue because:

  • Thread-1 is waiting for Thread-2 to release Lock-B.
  • Thread-2 is waiting for Thread-1 to release Lock-A.

As a result, both threads remain blocked.

Simple Example of Deadlock

Consider two objects:

Object lock1 = new Object();
Object lock2 = new Object();

Now suppose two threads acquire these locks in opposite orders.

Thread-1

synchronized (lock1) {
 
    synchronized (lock2) {
        // Work
    }
}

Thread-2

synchronized (lock2) {
 
    synchronized (lock1) {
        // Work
    }
}

This can create a deadlock.

How Deadlock Happens

Suppose Thread-1 starts first:

Thread-1
   ↓
Acquires lock1

Then Thread-2 starts:

Thread-2
   ↓
Acquires lock2

Now Thread-1 tries to acquire lock2:

Thread-1
   ↓
Waiting for lock2

At the same time, Thread-2 tries to acquire lock1:

Thread-2
   ↓
Waiting for lock1

The situation becomes:

Thread-1
   ↓
holds lock1
   ↓
waiting for lock2
          ↑
          |
       holds lock2
          |
       Thread-2
          ↓
     waiting for lock1

Neither thread can proceed.

This is a deadlock.

Complete Deadlock Example

public class HelloWorld {
 
    public static void main(String[] args) {
 
        Object lock1 = new Object();
        Object lock2 = new Object();
 
        Thread thread1 = new Thread(() -> {
 
            synchronized (lock1) {
 
                System.out.println(
                    "Thread-1 acquired lock1"
                );
 
                synchronized (lock2) {
 
                    System.out.println(
                        "Thread-1 acquired lock2"
                    );
                }
            }
        });
 
        Thread thread2 = new Thread(() -> {
 
            synchronized (lock2) {
 
                System.out.println(
                    "Thread-2 acquired lock2"
                );
 
                synchronized (lock1) {
 
                    System.out.println(
                        "Thread-2 acquired lock1"
                    );
                }
            }
        });
 
        thread1.start();
        thread2.start();
    }
}

Depending on thread scheduling, the program can reach a state where:

Thread-1 → holds lock1 → waits for lock2
 
Thread-2 → holds lock2 → waits for lock1

At that point, neither thread can continue.

Why Does Deadlock Occur?

Deadlock generally occurs when threads acquire multiple locks in an inconsistent order.

For example:

Thread-1:
Lock-A → Lock-B
 
Thread-2:
Lock-B → Lock-A

This creates the possibility of circular waiting.

A safer approach is to use the same lock acquisition order:

Thread-1:
Lock-A → Lock-B
 
Thread-2:
Lock-A → Lock-B

Now one thread may have to wait for the first lock, but the circular dependency is avoided.

Four Necessary Conditions for Deadlock

A classic deadlock analysis identifies four conditions that must exist together for a deadlock to occur.

These are:

  • Mutual exclusion
  • Hold and wait
  • No preemption
  • Circular wait

These conditions are often called the Coffman conditions.

1. Mutual Exclusion

A resource can be held by only one thread at a time.

For example, a monitor lock can be owned by only one thread at a time.

Lock-A
   ↓
Thread-1 owns the lock

Another thread cannot simultaneously own that same monitor.

2. Hold and Wait

A thread holds one resource while waiting for another resource.

For example:

Thread-1
   ↓
holds Lock-A
   ↓
waits for Lock-B

The thread does not release Lock-A while waiting for Lock-B.

3. No Preemption

A resource cannot simply be forcibly taken away from the thread that owns it.

The thread generally releases the monitor when it exits the synchronized region.

For example:

synchronized (lock) {
    // Thread owns lock
}

The monitor is released when the synchronized block is exited.

4. Circular Wait

A circular dependency exists between threads.

For example:

Thread-1
   ↓
waiting for Lock-B
   ↑
   |
held by
   |
Thread-2
   ↓
waiting for Lock-A
   ↑
   |
held by
   |
Thread-1

This creates a cycle:

Thread-1 → Lock-B → Thread-2 → Lock-A → Thread-1

The threads cannot make progress.

Deadlock Example Using Methods

Deadlock does not require explicit synchronized blocks. It can also occur with synchronized methods.

class Resource {
 
    public synchronized void methodA(Resource other) {
 
        System.out.println(
            Thread.currentThread().getName()
            + " entered methodA"
        );
 
        other.methodB();
    }
 
    public synchronized void methodB() {
 
        System.out.println(
            Thread.currentThread().getName()
            + " entered methodB"
        );
    }
}

Now:

Resource resource1 = new Resource();
Resource resource2 = new Resource();

One thread can call:

resource1.methodA(resource2);

while another calls:

resource2.methodA(resource1);

A deadlock can occur if:

  • Thread-1 holds resource1's monitor
  • Thread-2 holds resource2's monitor
  • Thread-1 waits for resource2's monitor
  • Thread-2 waits for resource1's monitor

Deadlock vs Race Condition

Deadlock and race condition are different problems.

DeadlockRace Condition
Threads wait indefinitelyResult depends on timing/interleaving
Threads may stop making progressThreads may continue but produce incorrect results
Usually involves conflicting resource acquisitionUsually involves unsafe concurrent access to shared state
Can cause application operations to hangCan cause inconsistent data
Often caused by circular lock dependenciesOften caused by missing synchronization or inadequate atomicity

Deadlock

Thread-1 → waits for Thread-2
Thread-2 → waits for Thread-1

Race Condition

Thread-1 ─┐
          ├→ shared data
Thread-2 ─┘
 
Timing affects the result

Deadlock vs Starvation

Deadlock is also different from starvation.

Deadlock

Threads are waiting for each other in a cycle.
Thread-1 → waits for Thread-2
Thread-2 → waits for Thread-1

Starvation

A thread cannot obtain the resources or CPU time it needs because other threads continually get access first.

Thread-1 → repeatedly gets resource
Thread-2 → keeps waiting

The thread may eventually proceed, but it is not receiving a fair opportunity to do so.

Deadlock vs Livelock

Another related problem is livelock.

Deadlock

Threads are blocked and cannot continue.

Thread-1 → waiting
Thread-2 → waiting

Livelock

Threads are active and repeatedly responding to each other, but no useful progress is made.

Conceptually:

Thread-1 → changes state
Thread-2 → responds
Thread-1 → changes again
Thread-2 → responds again

The threads are running, but the application is not making useful progress.

How to Prevent Deadlock

There is no single universal technique, but several design practices can significantly reduce deadlock risk.

1. Always Acquire Locks in the Same Order

This is one of the most common approaches.

Suppose an application uses:

Lock-A
Lock-B

Always acquire them in the same order:

Lock-A
  ↓
Lock-B

For example:

synchronized (lockA) {
 
    synchronized (lockB) {
        // Work
    }
}

Every thread that needs both locks should follow the same order.

Avoid:

synchronized (lockB) {
 
    synchronized (lockA) {
        // Work
    }
}

when another code path acquires lockA before lockB.

2. Avoid Unnecessary Nested Locks

The more locks a thread holds simultaneously, the more complicated lock dependencies become.

Instead of:

synchronized (lockA) {
 
    synchronized (lockB) {
 
        synchronized (lockC) {
            // Work
        }
    }
}

consider whether all three locks are actually necessary at the same time.

Reducing the number of simultaneously held locks can simplify concurrency design.

3. Keep Synchronized Sections Small

Avoid unnecessarily large synchronized sections.

Instead of:

synchronized (lock) {
 
    // Large amount of unrelated work
 
    // Shared resource access
 
    // More unrelated work
}

protect only the critical section when appropriate:

// Non-critical work
 
synchronized (lock) {
    // Shared resource access
}
 
// More non-critical work

This can reduce lock contention and make lock ownership easier to understand.

4. Avoid Calling External Code While Holding a Lock

Be careful when calling methods that you do not control while holding a lock.

For example:

synchronized (lock) {
 
    externalService.process();
 
}

If externalService.process() indirectly waits for another lock, the lock dependency can become difficult to predict.

Holding locks only when necessary can reduce this risk.

5. Use a Consistent Locking Strategy

A large application should have a clear approach to lock ownership.

For example:

Resource A → Lock-A
Resource B → Lock-B

Required order:

Lock-A → Lock-B

Following a consistent strategy makes concurrent code easier to reason about.

6. Use tryLock() When Appropriate

The Lock API provides methods that can attempt to acquire a lock without waiting indefinitely.

For example:

Lock lock = new ReentrantLock();
 
if (lock.tryLock()) {
 
    try {
        // Work
    } finally {
        lock.unlock();
    }
 
} else {
    // Could not acquire lock
}

Unlike an ordinary blocking lock acquisition, tryLock() can allow the application to choose what to do when the lock is unavailable.

Timed versions are also available.

Deadlock Prevention Using tryLock()

Consider two locks:

Lock lockA = new ReentrantLock();
Lock lockB = new ReentrantLock();

A thread can attempt to acquire both:

if (lockA.tryLock()) {
 
    try {
 
        if (lockB.tryLock()) {
 
            try {
                // Work with both locks
            } finally {
                lockB.unlock();
            }
 
        }
 
    } finally {
        lockA.unlock();
    }
}

A more complete design may use timed tryLock() calls and release already-acquired locks when the second lock cannot be obtained.

This approach can prevent a thread from waiting indefinitely for the second lock.

Deadlock Detection

Deadlocks can sometimes be detected using Java monitoring and diagnostic tools.

For example, Java's thread dump mechanisms can reveal threads waiting on monitors or locks.

A thread dump may show information such as:

Thread-1:
    waiting for lock held by Thread-2
 
Thread-2:
    waiting for lock held by Thread-1

This can help identify a circular dependency.

Java diagnostic tools such as jstack can be useful when investigating deadlocks in running JVM applications.

Example of a Deadlock Scenario

Consider two bank accounts:

Account A
Account B

A transfer from A to B might lock them in this order:

Lock Account A
Lock Account B
Transfer money
Unlock Account B
Unlock Account A

At the same time, another transfer from B to A might lock them in the opposite order:

Lock Account B
Lock Account A
Transfer money
Unlock Account A
Unlock Account B

This can create:

Transfer 1
   ↓
holds A
   ↓
waits for B
 
Transfer 2
   ↓
holds B
   ↓
waits for A

A common solution is to establish a consistent ordering for account locks.

For example: Always lock the account with the lower identity/order first.

Then both transfers follow the same lock ordering.

Deadlock and synchronized

Deadlock can occur when using synchronized.

For example:

synchronized (lockA) {
 
    synchronized (lockB) {
        // Work
    }
}

The problem is not that synchronized itself is incorrect.

The problem is how multiple locks are acquired by different threads.

Good synchronization design is therefore important.

Deadlock and ReentrantLock

Deadlock can also occur when using ReentrantLock.

For example:

lockA.lock();
 
try {
 
    lockB.lock();
 
    try {
        // Work
    } finally {
        lockB.unlock();
    }
 
} finally {
    lockA.unlock();
}

If another thread acquires lockB first and then waits for lockA, the same circular waiting problem can occur.

Using Lock does not automatically eliminate deadlocks.

Important: synchronized Does Not Automatically Cause Deadlock

It is incorrect to say:

"synchronized causes deadlock."

The synchronized mechanism provides controlled access to monitors.

Deadlock occurs because of a lock dependency pattern in which threads wait for resources held by each other.

For example:

Thread-1 → Lock-A → waits for Lock-B
Thread-2 → Lock-B → waits for Lock-A

The problem is the circular dependency.

How to Identify Deadlock in an Application

Common symptoms include:

  • Certain operations stop progressing.
  • Threads remain blocked for a long time.
  • CPU usage may be low even though requests are not completing.
  • Thread dumps show threads waiting for locks held by each other.
  • Requests may remain stuck indefinitely.
  • The application may appear responsive in some areas while specific operations remain blocked.

These symptoms alone do not prove a deadlock. Thread dumps and lock information should be used to confirm the condition.

Simple Deadlock Prevention Pattern

A useful design rule is:

Multiple Locks
     ↓
Define an Order
     ↓
Always Acquire
in That Order
     ↓
Release Locks
     ↓
Continue

For example:

Lock-A
  ↓
Lock-B
  ↓
Critical Section
  ↓
Unlock-B
  ↓
Unlock-A

Every thread that requires both locks should follow the same ordering.

Key Takeaways

  • Deadlock occurs when threads wait indefinitely for resources or locks held by each other.
  • Deadlock commonly involves multiple locks.
  • A circular dependency between threads can cause deadlock.
  • The four classic deadlock conditions are: mutual exclusion, hold and wait, no preemption, and circular wait.
  • Deadlock can occur with both synchronized and Lock APIs.
  • Consistent lock ordering is one of the common ways to prevent circular wait.
  • Avoid unnecessary nested locks.
  • Keep critical sections appropriately small.
  • Be careful when calling external code while holding locks.
  • tryLock() can be useful when indefinite waiting should be avoided.
  • Thread dumps can help diagnose deadlocks.
  • Deadlock is different from race condition, starvation, and livelock.
  • synchronized itself does not automatically cause deadlock; unsafe lock dependencies do.

Deadlock in One Diagram

            Thread-1
                |
                | holds
                v
              Lock-A
                |
                | waiting for
                v
              Lock-B
                ^
                | holds
                |
             Thread-2
                |
                | waiting for

v Lock-A

Circular dependency ↓ DEADLOCK

The key idea is: A deadlock occurs when threads become permanently blocked because each thread is waiting for a resource held by another thread in the same dependency cycle.

See you in next page....🙂

← Inter-thread CommunicationExecutor Framework →
← Inter-thread CommunicationExecutor Framework →