tech-notes-and-questions

Designing thread-safe classes

Thread safe classes are the classes whose objects (resources) can be shared by multiple threads and concurrent modifications are possible without any racing race conditions or data inconsistencies.

By following the below principles, you can design classes that behave correctly and safely in multi-threaded environments.

1. Immutable Classes

Immutable objects are inherently thread-safe because their state cannot be modified after construction. You can design your class to be immutable by:

2. Use Synchronization

Synchronization ensures that only one thread can execute a block of code at a time. This prevents race conditions when multiple threads access shared resources.

Advantages:

3. Use Locking Mechanisms

Java provides java.util.concurrent.locks package, which offers more advanced locking mechanisms like ReentrantLock, ReadWriteLock, etc compared to synchronized methods. These locks offer more flexibility than synchronized methods or blocks.

Note: In ReadWriteLock, it allows EITHER ReadLock for n users at a time OR WriteLock for one user at a time but not both.

Understanding through an example

Say we have a ticket booking system. Multiple users (threads) will try to view the chart and book a ticket. Reentrant Locking system would be causing slowness because viewing can be allowed to multiple users at a time since it won’t cause data inconsistencies. ReadWrite Locking would work better in this case.
Refer this for a working code example

Reentrant vs ReadWrite Locks

Advantages:

Disadvantages:

4. Use Atomic Variables

For simple atomic operations like increments and decrements, use classes from the java.util.concurrent.atomic package. Classes like AtomicInteger, AtomicLong, etc., provide lock-free thread safety for single variables.

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

5. Use Thread-safe Collections

Use thread-safe versions of collections, such as ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue, provided in the java.util.concurrent package. These collections handle synchronization internally and are designed for high-concurrency scenarios.

import java.util.concurrent.ConcurrentHashMap;

public class ThreadSafeCache {
    private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();

    public void put(String key, String value) {
        cache.put(key, value);
    }

    public String get(String key) {
        return cache.get(key);
    }
}

6. Minimize Lock Scope

Keep the scope of synchronized blocks as small as possible to reduce contention and improve performance. Only synchronize the critical section of the code that modifies shared state.

7. Avoid Deadlocks

Be mindful of potential deadlocks, where two or more threads are waiting on each other to release locks. Strategies to avoid deadlocks include:

Refer the code for a working example of a deadlock

8. Use ThreadLocal Variables

When each thread needs its own copy of a variable, use ThreadLocal. This ensures that each thread has its own independent instance of the variable, avoiding shared state issues.

public class ThreadLocalExample {
    private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public void setValue(int value) {
        threadLocalValue.set(value);
    }

    public int getValue() {
        return threadLocalValue.get();
    }
}

9. Use Higher-Level Concurrency Utilities

Java provides higher-level concurrency utilities like ExecutorService, ForkJoinPool, CountDownLatch, CyclicBarrier, etc. These abstractions help manage concurrency without needing to deal with low-level thread management directly.

10. Correctness over Performance

Focus on ensuring correctness first when designing thread-safe classes. Once correctness is ensured, you can optimize performance by reducing the scope of synchronization or using more advanced concurrency techniques.