Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Mastering Java Multithreading : Thread Control, Synchronization & Concurrency Utilities

by Ayush Shrivastava

.

Updated Thu Jul 31 2025

.
Mastering Java Multithreading : Thread Control, Synchronization & Concurrency Utilities

Chapter

Multithreading Part-2

Author

Ayush Shrivastava

Hub

Java Backend Development: Zero to Hero

Part-1

https://masteringbackend.com/posts/introduction-to-multithreading?ref=homepage

In part one, we explored the basics of Java multithreading, including threads, processes, and how to create threads using Thread and Runnable. In this second part, we’ll go deeper and cover essential concepts like thread control methods, synchronization, inter-thread communication, daemon threads, and Java concurrency utilities.

Mastering (7) (1).pngThread Control

Mastering (2) (1).pngJava provides built-in methods to control thread execution.

Key Methods:

  • sleep(): Pauses the current thread for a given time.

  • join(): Waits for another thread to finish.

  • yield(): Suggests that the current thread should pause for others to execute.

Example:

package ayshriv;

public class MasteringBackend {
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Thread 1: " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        t1.start();
        t1.join(); // Waits for t1 to finish

        System.out.println("Main thread finished after t1");
    }
}

In this above code, we used sleep() to pause execution inside the loop and join() to wait for t1 to finish before moving on. This ensures proper sequence control.

Output

Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Main thread finished after t1

Thread Synchronization

Mastering (3) (1).pngWhen multiple threads access shared data, synchronization is used to avoid race conditions.

Example Without Synchronization:

package ayshriv;

class Counter {
    int count = 0;
    public void increment() {
        count++;
    }
}

public class MasteringBackend {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Count: " + counter.count);
    }
}

In this above code, the final count may be less than 2000 due to race conditions because both threads are accessing and modifying count without synchronization.

Output

Count: 196

Example With Synchronization:

package ayshriv;

class SyncCounter {
    int count = 0;
    public synchronized void increment() {
        count++;
    }
}

public class MasteringBackend {
    public static void main(String[] args) throws InterruptedException {
        SyncCounter counter = new SyncCounter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Synchronized Count: " + counter.count);
    }
}

In this above code, we used the synchronized keyword to ensure only one thread can access increment() at a time, preventing data inconsistency.

Output

Synchronized Count: 2000

Inter-Thread Communication

Mastering (4) (1).pngJava provides wait(), notify(), and notifyAll() to allow threads to communicate and work together.

Example: Producer-Consumer

package ayshriv;

class SharedResource {
    private int data;
    private boolean hasData = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (hasData) wait();
        data = value;
        System.out.println("Produced: " + data);
        hasData = true;
        notify();
    }

    public synchronized void consume() throws InterruptedException {
        while (!hasData) wait();
        System.out.println("Consumed: " + data);
        hasData = false;
        notify();
    }
}

public class MasteringBackend {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();

        Thread producer = new Thread(() -> {
            try {
                resource.produce(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                resource.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

In this above code, the producer thread creates data, and the consumer thread waits for data to be available. This communication is controlled using wait() and notify().

Output

Produced: 10  
Consumed: 10

Daemon Threads

Mastering (5) (1).pngDaemon threads run in the background and do not prevent the JVM from exiting.

Example:

package ayshriv;

public class MasteringBackend {
    public static void main(String[] args) {
        Thread daemon = new Thread(() -> {
            while (true) {
                System.out.println("Background task running...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {}
            }
        });

        daemon.setDaemon(true);
        daemon.start();

        System.out.println("Main thread finished");
    }
}

In this above code, the daemon thread runs in the background, and once the main thread ends, the JVM exits, stopping the daemon thread too.

Output

Main thread finished  
Background task running...

Avoiding Deadlock

Mastering (6) (1).pngDeadlock happens when two or more threads are waiting for each other to release resources.

Example of Deadlock:

java
CopyEdit
package ayshriv;

public class MasteringBackend {
    public static void main(String[] args) {
        String lock1 = "LockA";
        String lock2 = "LockB";

        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1 locked LockA");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock2) {
                    System.out.println("Thread 1 locked LockB");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2 locked LockB");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock1) {
                    System.out.println("Thread 2 locked LockA");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

In this above code, both threads hold one lock and wait for the other, causing a deadlock. To avoid it, use a consistent locking order.

Output

Thread 1 locked LockA  
Thread 2 locked LockB

Java Concurrency Utilities

Java offers high-level concurrency tools in the java.util.concurrent package.

Using ExecutorService

java
CopyEdit
package ayshriv;

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

public class MasteringBackend {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        executor.submit(() -> System.out.println("Task 1 running"));
        executor.submit(() -> System.out.println("Task 2 running"));

        executor.shutdown(); // Don't forget to shut it down
    }
}

In this above code, we used ExecutorService to manage a thread pool, making thread creation and execution more efficient in large applications.

Course image
Become a Java Backend Engineeer today

All-in-one Java course for learning backend engineering with Java. This comprehensive course is designed for Java developers seeking proficiency in Java.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week