Mastering Concurrency, Multithreading, and Synchronization in Java – A Step-by-Step Guide

DevCorner - Feb 15 - - Dev Community

Concurrency and multithreading are fundamental concepts for any backend or system-level developer. They play a critical role in building high-performance, scalable, and robust applications. This detailed, step-by-step guide will help you master these concepts with practical examples and best practices.


1. Introduction to Concurrency and Multithreading

What is Concurrency?

Concurrency is the ability of a program to perform multiple tasks simultaneously. It allows different parts of a program to run independently, enhancing performance and responsiveness.

What is Multithreading?

Multithreading is a specific form of concurrency where multiple threads run in parallel within a single program. Each thread represents a separate path of execution.

Concept Definition
Process An independent program with its memory space.
Thread A lightweight sub-process sharing the same memory space.
Concurrency Managing multiple tasks simultaneously.
Parallelism Actual execution of multiple tasks at the same time.

2. Creating Threads in Java

2.1. Extending Thread Class

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running...");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

2.2. Implementing Runnable Interface

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable thread is running...");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practice: Prefer Runnable over Thread because it promotes better design by decoupling task definition from thread execution.


3. Thread Lifecycle

State Description
New Thread is created but not started.
Runnable Thread is ready to run, waiting for CPU time.
Blocked Waiting to acquire a lock.
Waiting Indefinitely waiting for another thread's signal.
Timed Waiting Waiting for a specified time.
Terminated Thread has finished execution.

4. Concurrency Problems

Common Issues

  • Race Condition – Two or more threads accessing shared data leading to inconsistent results.
  • Deadlock – Two or more threads are waiting for each other indefinitely.
  • Livelock – Threads keep changing states but make no progress.
  • Starvation – A thread is denied CPU access due to other high-priority threads.

5. Synchronization in Java

Synchronization is the process of controlling the access of multiple threads to shared resources.

5.1. synchronized Keyword

Synchronizes a method or block to allow only one thread to access it at a time.

5.1.1. Synchronized Method

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
Enter fullscreen mode Exit fullscreen mode

5.1.2. Synchronized Block

class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}
Enter fullscreen mode Exit fullscreen mode

5.2. Lock Interface

Provides more control compared to synchronized.

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

class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
Enter fullscreen mode Exit fullscreen mode
Approach Flexibility Performance
synchronized Less Generally Fast
Lock High Slightly Slower

6. Advanced Concurrency Tools

6.1. volatile Keyword

Ensures visibility of changes to variables across threads.

private volatile boolean running = true;
Enter fullscreen mode Exit fullscreen mode

6.2. Atomic Variables

Provides thread-safe operations without locking.

import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
Enter fullscreen mode Exit fullscreen mode

6.3. Executors Framework

Creates and manages thread pools.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.execute(() -> System.out.println("Task executed"));
executor.shutdown();
Enter fullscreen mode Exit fullscreen mode
Executor Type Description
newFixedThreadPool(n) Pool with a fixed number of threads.
newCachedThreadPool() Expands as needed, reuses idle threads.
newSingleThreadExecutor() Single-thread executor.

7. Thread Communication

7.1. wait() and notify()

Used for inter-thread communication within synchronized context.

synchronized (lock) {
    lock.wait();  // Releases the lock and waits
    lock.notify(); // Wakes up a waiting thread
}
Enter fullscreen mode Exit fullscreen mode
Method Description
wait() Makes the current thread wait.
notify() Wakes up a single waiting thread.
notifyAll() Wakes up all waiting threads.

8. Deadlock Example

class A {
    synchronized void foo(B b) {
        System.out.println("Thread1 trying to call B's last()");
        b.last();
    }

    synchronized void last() {
        System.out.println("Inside A's last()");
    }
}

class B {
    synchronized void bar(A a) {
        System.out.println("Thread2 trying to call A's last()");
        a.last();
    }

    synchronized void last() {
        System.out.println("Inside B's last()");
    }
}
Enter fullscreen mode Exit fullscreen mode

Avoid deadlocks using lock ordering or tryLock() from ReentrantLock.


9. Best Practices for Multithreading

  • Use ExecutorService instead of manually creating threads.
  • Minimize shared data between threads.
  • Use synchronized, Locks, volatile, and Atomic wisely.
  • Avoid unnecessary synchronization as it can degrade performance.
  • Identify thread-safety requirements early in development.
  • Test for concurrency issues using stress tests and tools like FindBugs.

10. Common Interview Questions

  1. Difference between synchronized and Lock?
  2. What is volatile and when to use it?
  3. How to avoid deadlocks?
  4. What is a thread-safe class in Java?
  5. Explain thread pool benefits.

Conclusion

Concurrency, multithreading, and synchronization are crucial for building robust and high-performance applications. Understanding these concepts in depth will help you write efficient, thread-safe, and scalable software. Practice writing concurrent programs, experiment with different synchronization techniques, and always consider thread-safety when dealing with shared resources.


Next Steps:

  • Implement thread-safe classes using synchronized and Lock.
  • Use ExecutorService in real projects.
  • Explore advanced concurrent utilities like CountDownLatch, Semaphore, and CyclicBarrier.

Happy Coding!


Would you like diagrams or code snippets with more real-world scenarios for your blog?

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .