BLOG POSTS
Deadlock in Java: Example and Explanation

Deadlock in Java: Example and Explanation

Deadlock in Java is one of those concurrency nightmares that can bring your multithreaded application to a complete standstill. It occurs when two or more threads are blocked forever, each waiting for the other to release a resource they need. Understanding deadlocks is crucial for any developer working with concurrent applications, especially those running on production servers where performance and reliability are paramount. This post will walk you through the technical mechanics of deadlocks, provide concrete examples, and give you practical strategies to detect, prevent, and resolve these issues in your Java applications.

What Is a Deadlock and How It Works

A deadlock happens when multiple threads are stuck in a circular dependency, each holding a resource that another thread needs. Think of it like two cars approaching each other on a narrow bridge – neither can proceed because the other is blocking the way.

For a deadlock to occur, four conditions must be met simultaneously:

  • Mutual Exclusion: Resources cannot be shared between threads
  • Hold and Wait: A thread holds at least one resource while waiting for another
  • No Preemption: Resources cannot be forcibly taken from threads
  • Circular Wait: A circular chain of threads exists where each waits for a resource held by the next

In Java, deadlocks commonly occur with synchronized blocks, explicit locks, or when threads acquire multiple locks in different orders.

Classic Deadlock Example

Here’s a straightforward example that demonstrates a deadlock scenario:

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Acquired lock1");
                
                try {
                    Thread.sleep(100); // Simulate some work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                
                System.out.println("Thread 1: Waiting for lock2");
                synchronized (lock2) {
                    System.out.println("Thread 1: Acquired lock2");
                }
            }
        });
        
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Acquired lock2");
                
                try {
                    Thread.sleep(100); // Simulate some work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                
                System.out.println("Thread 2: Waiting for lock1");
                synchronized (lock1) {
                    System.out.println("Thread 2: Acquired lock1");
                }
            }
        });
        
        thread1.start();
        thread2.start();
        
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        System.out.println("Program completed");
    }
}

When you run this code, you’ll see the first two print statements from each thread, but the program will hang indefinitely. Thread 1 holds lock1 and waits for lock2, while Thread 2 holds lock2 and waits for lock1.

Real-World Banking System Example

Let’s look at a more realistic scenario – a banking system where deadlocks can occur during fund transfers:

public class BankAccount {
    private final String accountId;
    private double balance;
    private final Object lock = new Object();
    
    public BankAccount(String accountId, double initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }
    
    public void transfer(BankAccount to, double amount) {
        synchronized (this.lock) {
            System.out.println("Thread " + Thread.currentThread().getName() + 
                             " acquired lock for account " + this.accountId);
            
            try {
                Thread.sleep(50); // Simulate processing time
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            
            synchronized (to.lock) {
                System.out.println("Thread " + Thread.currentThread().getName() + 
                                 " acquired lock for account " + to.accountId);
                
                if (this.balance >= amount) {
                    this.balance -= amount;
                    to.balance += amount;
                    System.out.println("Transferred " + amount + " from " + 
                                     this.accountId + " to " + to.accountId);
                }
            }
        }
    }
    
    public double getBalance() {
        synchronized (lock) {
            return balance;
        }
    }
    
    public String getAccountId() {
        return accountId;
    }
}

public class BankingDeadlockDemo {
    public static void main(String[] args) {
        BankAccount account1 = new BankAccount("ACC001", 1000);
        BankAccount account2 = new BankAccount("ACC002", 1000);
        
        Thread t1 = new Thread(() -> {
            account1.transfer(account2, 100);
        }, "Transfer-1");
        
        Thread t2 = new Thread(() -> {
            account2.transfer(account1, 200);
        }, "Transfer-2");
        
        t1.start();
        t2.start();
        
        try {
            t1.join(5000); // Wait max 5 seconds
            t2.join(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        if (t1.isAlive() || t2.isAlive()) {
            System.out.println("Deadlock detected! Threads are still running.");
        }
    }
}

Deadlock Detection Techniques

Java provides built-in tools for detecting deadlocks in running applications:

Using ThreadMXBean for Programmatic Detection

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    private static final ThreadMXBean threadBean = 
        ManagementFactory.getThreadMXBean();
    
    public static void detectDeadlock() {
        long[] threadIds = threadBean.findDeadlockedThreads();
        
        if (threadIds != null) {
            System.out.println("Deadlock detected!");
            ThreadInfo[] threadInfos = threadBean.getThreadInfo(threadIds);
            
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println("Thread: " + threadInfo.getThreadName());
                System.out.println("State: " + threadInfo.getThreadState());
                System.out.println("Blocked on: " + threadInfo.getLockName());
                System.out.println("Lock owner: " + threadInfo.getLockOwnerName());
                System.out.println("---");
            }
        } else {
            System.out.println("No deadlock detected");
        }
    }
    
    public static void monitorForDeadlocks() {
        Thread monitor = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                detectDeadlock();
                try {
                    Thread.sleep(5000); // Check every 5 seconds
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        
        monitor.setDaemon(true);
        monitor.start();
    }
}

Command Line Tools

You can also use JVM tools to detect deadlocks:

# Get the PID of your Java application
jps

# Generate a thread dump
jstack [PID] > threaddump.txt

# Or use jcmd
jcmd [PID] Thread.print > threaddump.txt

Prevention Strategies

Ordered Lock Acquisition

The most effective prevention technique is to always acquire locks in the same order:

public class OrderedBankAccount {
    private final String accountId;
    private final int hashCode;
    private double balance;
    private final Object lock = new Object();
    
    public OrderedBankAccount(String accountId, double initialBalance) {
        this.accountId = accountId;
        this.hashCode = System.identityHashCode(this);
        this.balance = initialBalance;
    }
    
    public void transfer(OrderedBankAccount to, double amount) {
        OrderedBankAccount firstLock = this.hashCode < to.hashCode ? this : to;
        OrderedBankAccount secondLock = this.hashCode < to.hashCode ? to : this; synchronized (firstLock.lock) { synchronized (secondLock.lock) { if (this.balance >= amount) {
                    this.balance -= amount;
                    to.balance += amount;
                    System.out.println("Transferred " + amount + " from " + 
                                     this.accountId + " to " + to.accountId);
                }
            }
        }
    }
}

Using Timeout-Based Locks

Java’s java.util.concurrent.locks package provides more sophisticated locking mechanisms:

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class TimeoutBankAccount {
    private final String accountId;
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();
    
    public TimeoutBankAccount(String accountId, double initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }
    
    public boolean transfer(TimeoutBankAccount to, double amount) {
        boolean fromLocked = false;
        boolean toLocked = false;
        
        try {
            fromLocked = this.lock.tryLock(1000, TimeUnit.MILLISECONDS);
            if (!fromLocked) {
                return false;
            }
            
            toLocked = to.lock.tryLock(1000, TimeUnit.MILLISECONDS);
            if (!toLocked) {
                return false;
            }
            
            if (this.balance >= amount) {
                this.balance -= amount;
                to.balance += amount;
                System.out.println("Transferred " + amount + " from " + 
                                 this.accountId + " to " + to.accountId);
                return true;
            }
            
            return false;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (toLocked) {
                to.lock.unlock();
            }
            if (fromLocked) {
                this.lock.unlock();
            }
        }
    }
}

Comparison of Deadlock Prevention Techniques

Technique Pros Cons Best Use Case
Ordered Lock Acquisition Simple, guaranteed prevention, no performance overhead Requires global ordering strategy, can be complex with many locks Systems with predictable lock patterns
Timeout-Based Locks Flexible, allows retry logic, good for distributed systems May introduce latency, requires error handling High-throughput systems where occasional failures are acceptable
Lock-Free Programming Maximum performance, no deadlock risk Complex implementation, limited use cases High-performance concurrent data structures
Single Lock Approach Simple, no deadlock possible Poor concurrency, potential bottleneck Simple applications with low concurrency requirements

Advanced Deadlock Resolution with Lock-Free Algorithms

For high-performance applications, consider using atomic operations and lock-free data structures:

import java.util.concurrent.atomic.AtomicLong;

public class AtomicBankAccount {
    private final String accountId;
    private final AtomicLong balance;
    
    public AtomicBankAccount(String accountId, long initialBalance) {
        this.accountId = accountId;
        this.balance = new AtomicLong(initialBalance);
    }
    
    public boolean transfer(AtomicBankAccount to, long amount) {
        while (true) {
            long currentBalance = this.balance.get();
            
            if (currentBalance < amount) {
                return false; // Insufficient funds
            }
            
            // Try to update source account
            if (this.balance.compareAndSet(currentBalance, currentBalance - amount)) {
                // Successfully deducted, now add to destination
                to.balance.addAndGet(amount);
                System.out.println("Transferred " + amount + " from " + 
                                 this.accountId + " to " + to.accountId);
                return true;
            }
            // If CAS failed, retry
        }
    }
    
    public long getBalance() {
        return balance.get();
    }
}

Best Practices and Production Considerations

When deploying multithreaded Java applications on production servers, especially on VPS or dedicated servers, consider these best practices:

  • Always use consistent lock ordering: Establish a global ordering for all locks in your application
  • Keep synchronized blocks small: Minimize the time spent holding locks
  • Use higher-level concurrency utilities: Prefer java.util.concurrent classes over low-level synchronization
  • Implement timeout mechanisms: Use tryLock() with timeouts for critical operations
  • Monitor for deadlocks: Implement automatic deadlock detection in production
  • Use thread dumps for debugging: Regularly capture and analyze thread dumps
  • Design for failure: Implement retry logic and graceful degradation

Monitoring Deadlocks in Production

Here’s a production-ready deadlock monitoring utility:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import java.util.logging.Level;

public class ProductionDeadlockMonitor {
    private static final Logger logger = Logger.getLogger(ProductionDeadlockMonitor.class.getName());
    private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    public void startMonitoring(int intervalSeconds) {
        scheduler.scheduleAtFixedRate(this::checkForDeadlocks, 0, intervalSeconds, TimeUnit.SECONDS);
        logger.info("Deadlock monitoring started with " + intervalSeconds + "s interval");
    }
    
    private void checkForDeadlocks() {
        try {
            long[] deadlockedThreads = threadBean.findDeadlockedThreads();
            
            if (deadlockedThreads != null && deadlockedThreads.length > 0) {
                StringBuilder report = new StringBuilder();
                report.append("DEADLOCK DETECTED! Affected threads:\n");
                
                for (long threadId : deadlockedThreads) {
                    var threadInfo = threadBean.getThreadInfo(threadId);
                    report.append("Thread: ").append(threadInfo.getThreadName())
                          .append(" (ID: ").append(threadId).append(")\n")
                          .append("State: ").append(threadInfo.getThreadState()).append("\n")
                          .append("Blocked on: ").append(threadInfo.getLockName()).append("\n")
                          .append("Stack trace:\n");
                    
                    for (var stackFrame : threadInfo.getStackTrace()) {
                        report.append("  at ").append(stackFrame).append("\n");
                    }
                    report.append("\n");
                }
                
                logger.severe(report.toString());
                
                // Optional: trigger alerts, metrics, or automatic recovery
                handleDeadlockDetected(deadlockedThreads);
            }
        } catch (Exception e) {
            logger.log(Level.WARNING, "Error during deadlock detection", e);
        }
    }
    
    private void handleDeadlockDetected(long[] threadIds) {
        // Implement your alerting logic here
        // Examples: send metrics to monitoring system, trigger notifications, etc.
        System.err.println("ALERT: Deadlock detected involving " + threadIds.length + " threads");
    }
    
    public void shutdown() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

Performance Impact and Benchmarking

Different deadlock prevention strategies have varying performance characteristics. Here’s a simple benchmark comparing approaches:

public class DeadlockPreventionBenchmark {
    private static final int THREAD_COUNT = 10;
    private static final int OPERATIONS_PER_THREAD = 1000;
    
    public static void main(String[] args) throws InterruptedException {
        benchmarkSynchronized();
        benchmarkReentrantLock();
        benchmarkAtomicOperations();
    }
    
    private static void benchmarkSynchronized() throws InterruptedException {
        System.out.println("Benchmarking synchronized approach...");
        long start = System.nanoTime();
        
        Object[] locks = new Object[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            locks[i] = new Object();
        }
        
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) { final int threadIndex = i; threads[i] = new Thread(() -> {
                for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
                    int lock1Index = Math.min(threadIndex, (threadIndex + 1) % THREAD_COUNT);
                    int lock2Index = Math.max(threadIndex, (threadIndex + 1) % THREAD_COUNT);
                    
                    synchronized (locks[lock1Index]) {
                        synchronized (locks[lock2Index]) {
                            // Simulate work
                            Thread.yield();
                        }
                    }
                }
            });
        }
        
        for (Thread thread : threads) {
            thread.start();
        }
        
        for (Thread thread : threads) {
            thread.join();
        }
        
        long duration = System.nanoTime() - start;
        System.out.println("Synchronized: " + duration / 1_000_000 + " ms");
    }
    
    // Similar methods for other approaches...
}

Understanding and preventing deadlocks is essential for building robust concurrent Java applications. Whether you’re running your applications on cloud infrastructure or dedicated hardware, implementing proper deadlock prevention strategies will ensure your systems remain responsive and reliable under load. Regular monitoring and proactive detection can help you identify potential issues before they impact your users.

For more detailed information about Java concurrency, check out the official Java concurrency tutorial and the java.util.concurrent package documentation.



This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.

This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.

Leave a reply

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