Friday, September 22, 2023

14 Multithreading best practices in Java for Experienced Developers

Hello guys, if you are doing Java development then you know how important your multithreading and concurrency skills are. They are often the deciding factor in your selection or non-selection and that's why its very important to continuously improve your Java concurrency and multithreading knowledge. In the past, I have shared 50+ thread interview questions and today's article, I am going to share 15 best multithreading practices which experienced Java developer should know and follow. 










1) Given meaningful name to your threads and thread pool


I can't emphasize this. When dumping threads of a running JVM or during debugging, default thread pool naming scheme is pool-N-thread-M, where N stands for pool sequence number (every time you create a new thread pool, global N counter is incremented) and M is a thread sequence number within a pool. For example pool-2-thread-3 means third thread in second pool created in the JVM lifecycle. See: Executors.defaultThreadFactory(). Not very descriptive. JDK makes it slightly complex to properly name threads because naming strategy is hidden inside ThreadFactory. Luckily Guava has a helper class for that:

import com.google.common.util.concurrent.ThreadFactoryBuilder;


final ThreadFactory threadFactory = new ThreadFactoryBuilder()

        .setNameFormat("Orders-%d")

        .setDaemon(true)

        .build();

final ExecutorService executorService = Executors.newFixedThreadPool(10, threadFactory);

By default thread pools create non-daemon threads, decide whether this suits you or not.


Let's go through each of these multithreading best practices with examples to illustrate their importance:


2) Minimize inter thread communication and data sharing

Inter-thread communication and data sharing can lead to complex synchronization issues. Minimizing such communication reduces the risk of deadlocks and race conditions. Consider using thread-local variables when possible to avoid sharing data between threads.


Example:


public class ThreadLocalExample {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);


    public static void main(String[] args) {

        threadLocal.set(42);

        int result = threadLocal.get(); // Each thread has its own value

    }

}

3) Minimize locking by reducing the scope of synchronized

To avoid contention and improve performance, synchronize only the critical sections of your code, reducing the scope of synchronization.


Example:

public class SynchronizationExample {

    private int count = 0;

    private Object lock = new Object();


    public void increment() {

        synchronized (lock) {

            count++;

        }

    }

}

4) Use Immutable objects

Immutable objects are thread-safe by design because they cannot be modified once created. Use them to avoid the need for synchronization when dealing with shared data.


Example:


public final class ImmutableExample {

    private final int value;


    public ImmutableExample(int value) {

        this.value = value;

    }


    public int getValue() {

        return value;

    }

}

5) Avoid Static variables

Static variables can be shared among all threads, leading to concurrency issues. Minimize their use and prefer instance variables when necessary.


Example:


public class StaticVariableExample {

    private static int sharedCount = 0;


    public synchronized static void increment() {

        sharedCount++;

    }

}

6) Use MDC Context for Logging

Logging frameworks like Log4j and Logback provide tools like Mapped Diagnostic Context (MDC) to associate context information with log entries for each thread.


Example (Logback MDC):


import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.MDC;


public class LoggingExample {

    private static final Logger logger = LoggerFactory.getLogger(LoggingExample.class);


    public static void main(String[] args) {

        MDC.put("user", "john_doe");

        logger.info("User logged in.");

        MDC.clear();

    }

}


7) Always remove ThreadLocal once done

When using ThreadLocal, it's essential to remove the value associated with the current thread once you're done with it to prevent memory leaks.


Example:


public class ThreadLocalExample {

    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);


    public static void main(String[] args) {

        threadLocal.set(42);

        int result = threadLocal.get();

        threadLocal.remove(); // Remove the value when done

    }

}


8) Use Lock Stripping (e.g., ReadWriteLock)

Lock stripping techniques, like ReadWriteLock, allow multiple threads to read concurrently while ensuring exclusive access for writes.


Example:


import java.util.concurrent.locks.ReadWriteLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;


public class ReadWriteLockExample {

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    private int data;


    public int readData() {

        lock.readLock().lock();

        try {

            return data;

        } finally {

            lock.readLock().unlock();

        }

    }


    public void writeData(int newData) {

        lock.writeLock().lock();

        try {

            data = newData;

        } finally {

            lock.writeLock().unlock();

        }

    }

}


9) Use volatile

The volatile keyword ensures that reads and writes to a variable are directly performed on the main memory, preventing thread-local caching of the variable.


Example:

public class VolatileExample {

    private volatile boolean flag = false;


    public void toggleFlag() {

        flag = !flag;

    }

}


10) Use atomic integers for counters:

Atomic integer classes, such as AtomicInteger, provide thread-safe operations for common tasks like incrementing counters.


Example:

import java.util.concurrent.atomic.AtomicInteger;


public class AtomicIntegerExample {

    private AtomicInteger counter = new AtomicInteger(0);


    public void increment() {

        counter.incrementAndGet();

    }


    public int getCount() {

        return counter.get();

    }

}

11) Use BlockingQueue for Producer-Consumer design:

BlockingQueue provides a thread-safe way to implement the Producer-Consumer pattern, ensuring proper synchronization between producers and consumers.


Example:

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;


public class ProducerConsumerExample {

    private BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);


    public void produce(int item) throws InterruptedException {

        queue.put(item);

    }


    public int consume() throws InterruptedException {

        return queue.take();

    }

}

12) Use Disruptor for fast inter-thread communication:

The Disruptor pattern is designed for high-performance, low-latency inter-thread communication. It offers a ring buffer-based approach for handling data flow between threads efficiently.


13) Minimize context switching of threads:

Reducing context switching, where a CPU switches between threads, improves performance. Minimize unnecessary thread creation and ensure efficient use of existing threads.


14) Use Busy Spin to keep cached data in CPU L1:

Busy spinning is a technique where a thread repeatedly checks for a condition to become true instead of yielding its execution. It can be used when waiting times are short to keep data in CPU caches and reduce latency.


15) Use thread-safety:

Always design your classes and components with thread-safety in mind. Understand the thread-safety guarantees of libraries and frameworks you use and follow best practices to ensure your code is robust in a multi-threaded environment.


In conclusion, these best practices help you write efficient and robust multi-threaded Java applications. By minimizing inter-thread communication, using thread-safe constructs, and following these guidelines, you can avoid common pitfalls and create reliable concurrent systems.

No comments:

Post a Comment

Feel free to comment, ask questions if you have any doubt.