Best Practices for Java Multithreading Methods


In today's computing landscape, leveraging multithreading in Java is essential for building responsive and scalable applications. However, with the power of concurrency comes the complexity of managing shared resources and ensuring thread safety.

Understanding and implementing best practices in Java multithreading is crucial to avoid pitfalls like race conditions, deadlocks, and performance bottlenecks.

This article explores essential techniques such as synchronization with synchronized blocks, using reentrant locks for finer-grained control, and coordinating access with semaphores from java.util.concurrent. By incorporating these mechanisms, developers can effectively manage shared resources and thread interactions, enhancing application reliability and performance.



A semaphore in Java is used to control access to a resource by multiple threads. It works by maintaining a set of permits, and threads can acquire or release permits.


Java multithreading methods example

Below example demonstrates the Sempaphore in Java Threads

Note:

If java is not intstalled on your system, here is the quick guide to install Java 17 - OpenJDK Java 17

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final Semaphore semaphore = new Semaphore(3);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Worker()).start();
        }
    }

    static class Worker implements Runnable {
        @Override
        public void run() {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " acquired a permit.");
                Thread.sleep(2000); // Simulate work
                System.out.println(Thread.currentThread().getName() + " released a permit.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
            }
        }
    }
}

        

Checkout SemaphoreThread.java on GitHub




ReentrantLock in Java offers more flexibility than the synchronized keyword. It provides methods for lock polling, timed lock waits, and interruptible lock acquisition.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();
    private static int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Worker()).start();
        }
    }

    static class Worker implements Runnable {
        @Override
        public void run() {
            lock.lock();
            try {
                counter++;
                System.out.println(Thread.currentThread().getName() + " incremented counter to " + counter);
            } finally {
                lock.unlock();
            }
        }
    }
}

        

Checkout RentrantLockExample.java on GitHub




The synchronized keyword in Java is used for simple mutual exclusion, allowing only one thread to execute a block of code at a time.

public class SynchronizedExample {
    private static int counter = 0;

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> example.incrementCounter()).start();
        }
    }

    public synchronized void incrementCounter() {
        counter++;
        System.out.println(Thread.currentThread().getName() + " incremented counter to " + counter);
    }
}

        



Reentrancy means that a thread can acquire the same lock multiple times. This is useful when a method that holds a lock calls another method that also needs the same lock.

public class ReentrantExample {
    public synchronized void method1() {
        System.out.println("method1");
        method2(); // Re-acquires the lock
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.method1(); // Calls method1, which in turn calls method2
    }
}

        



The following example demonstrates a ReentrantLock being re-acquired by the same thread in different methods:

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void method1() {
        lock.lock();
        try {
            System.out.println("method1");
            method2(); // Re-acquires the lock
        } finally {
            lock.unlock();
        }
    }

    public void method2() {
        lock.lock();
        try {
            System.out.println("method2");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        example.method1(); // Calls method1, which in turn calls method2
    }
}

        



This article covers the basics of thread synchronization in Java, including the use of semaphores, ReentrantLock, and synchronized locks. Each mechanism is explained with practical examples, demonstrating how to control access to shared resources, ensure thread safety, and understand reentrancy. The differences between these synchronization techniques are highlighted to help you choose the right tool for your concurrency needs.


Checkout complete code on GitHub



Read Next