Multi Threading - rahul7838/quora.clone GitHub Wiki
A mutex allows only a single thread to access a resource or critical section. Critical section(shared data structure or resource) of code to be guarded from multiple access to avoid data corruption and race condition. Here are different ways
I'll explain the different ways to achieve thread safety in Java:
- Synchronized Keyword
// Method level
public synchronized void method() {
// thread-safe code
}
// Block level
public void method() {
synchronized(this) {
// thread-safe code
}
}
- Volatile Keyword
public class Counter {
private volatile long count = 0;
public void increment() {
count++; // Atomic read and write
}
}
- Atomic Classes
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Thread-safe increment
}
public void decrement() {
count.decrementAndGet();
}
}
- Lock Interface Implementations
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}
- ReadWriteLock (for scenarios with multiple readers, few writers)
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private ReadWriteLock lock = new ReentrantReadWriteLock();
private Map<String, String> data = new HashMap<>();
public String read(String key) {
lock.readLock().lock();
try {
return data.get(key);
} finally {
lock.readLock().unlock();
}
}
public void write(String key, String value) {
lock.writeLock().lock();
try {
data.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}
- Thread-Safe Collections
// Synchronized Collections
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
// Concurrent Collections
ConcurrentHashMap<String, String> concurrentMap = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();
- Immutable Objects
public final class ImmutablePerson {
private final String name;
private final int age;
public ImmutablePerson(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
}
- ThreadLocal (for thread-confined state)
public class ThreadSafeFormatter {
private static final ThreadLocal<SimpleDateFormat> dateFormatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String format(Date date) {
return dateFormatter.get().format(date);
}
}
Key considerations when choosing a thread-safety mechanism:
-
Performance Requirements
- Synchronized blocks can impact performance
- Atomic operations are generally faster than locks
- ReadWriteLock is better when reads outnumber writes
-
Granularity Needed
- Method-level vs block-level synchronization
- Object-level vs class-level thread safety
-
Complexity vs Maintainability
- Immutable objects are easiest to maintain
- Lock-based solutions require careful management
- Thread-safe collections provide built-in safety
-
Scalability
- ConcurrentHashMap scales better than synchronized HashMap
- ReadWriteLock better for multiple readers
- Immutable objects scale perfectly
Sometime mutual exclusion is not enough and we need some signalling mechanism between the thread for example in consumer/producer problem.
here are ways for signalling
I'll help explain signaling across threads in Kotlin. Yes, Kotlin does provide several mechanisms for thread signaling, including semaphores through Java's concurrent utilities.
Here are the main ways to handle thread signaling in Kotlin:
- Semaphores
import java.util.concurrent.Semaphore
class SharedResource {
private val semaphore = Semaphore(1) // Create with initial permits
fun doWork() {
try {
semaphore.acquire() // Wait for permit
// Do critical work here
} finally {
semaphore.release() // Release permit
}
}
}
- CountDownLatch - Useful for waiting for a set number of operations to complete:
import java.util.concurrent.CountDownLatch
class Worker(private val startSignal: CountDownLatch, private val doneSignal: CountDownLatch) {
fun doWork() {
startSignal.await() // Wait for start signal
// Do work here
doneSignal.countDown() // Signal completion
}
}
// Usage
val startSignal = CountDownLatch(1)
val doneSignal = CountDownLatch(N) // N is number of worker threads
- Condition Variables using ReentrantLock:
import java.util.concurrent.locks.ReentrantLock
import java.util.concurrent.locks.Condition
class BoundedQueue<T> {
private val lock = ReentrantLock()
private val notFull: Condition = lock.newCondition()
private val notEmpty: Condition = lock.newCondition()
fun put(item: T) {
lock.lock()
try {
while (isFull()) {
notFull.await() // Wait until queue is not full
}
// Add item
notEmpty.signal() // Signal that queue is not empty
} finally {
lock.unlock()
}
}
}
- Coroutine Channels - Modern approach using Kotlin coroutines:
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.*
suspend fun producer(channel: Channel<Int>) {
for (i in 1..5) {
channel.send(i) // Send signal
}
channel.close()
}
suspend fun consumer(channel: Channel<Int>) {
for (item in channel) {
// Receive signal
println("Received: $item")
}
}
The choice between these mechanisms depends on your specific needs:
- Use Semaphores when you need to control access to a fixed number of resources
- Use CountDownLatch for one-time synchronization events
- Use Condition Variables when you need complex signaling logic with mutual exclusion
- Use Coroutine Channels for structured concurrency with more readable code