DEV Community

Jane for Mastering Backend

Posted on • Originally published at blog.masteringbackend.com

Mastering Java Multithreading : Thread Control, Synchronization & Concurrency Utilities

Mastering Java Multithreading : Thread Control, Synchronization & Concurrency Utilities

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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...
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Use our Online Code Editor

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 Engineer today

All-in-one Java course for learning backend engineering. Designed for Java developers seeking proficiency.

Start Learning Now

Have a great one!!!

Author: Ayush Shrivastava


Thank you for being a part of the community

Before you go:

Whenever you’re ready

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

  • The MB Platform: Join thousands of 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 Boot Camp to produce great backend engineers.
  • Join Backend Weekly: If you like posts 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.

Top comments (0)