0% found this document useful (0 votes)
98 views33 pages

Multithreading and Concurrency Interview Preparation Guide (Expanded)

The document serves as a comprehensive guide for multithreading and concurrency concepts in Java, covering key topics such as the volatile keyword, synchronized blocks, ReentrantLock, and various synchronization aids like CountDownLatch and CyclicBarrier. It explains the Java Memory Model (JMM) and common concurrency problems like the ABA problem, providing Java examples for practical understanding. This guide is essential for preparing for interviews focused on Java concurrency and multithreading principles.

Uploaded by

Prateek Gupta
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
98 views33 pages

Multithreading and Concurrency Interview Preparation Guide (Expanded)

The document serves as a comprehensive guide for multithreading and concurrency concepts in Java, covering key topics such as the volatile keyword, synchronized blocks, ReentrantLock, and various synchronization aids like CountDownLatch and CyclicBarrier. It explains the Java Memory Model (JMM) and common concurrency problems like the ABA problem, providing Java examples for practical understanding. This guide is essential for preparing for interviews focused on Java concurrency and multithreading principles.

Uploaded by

Prateek Gupta
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Multithreading and Concurrency Interview Preparation Guide

🔹 Core Java Concurrency Concepts


1.​ volatile Keyword
○​ Explanation:
■​ The volatile keyword ensures that a variable's value is always read from
and written to main memory, not the CPU cache. This addresses visibility
issues where different threads might have inconsistent views of a shared
variable.
■​ It does not guarantee atomicity. Operations like x++ are not atomic, even if
x is volatile. Multiple threads can still read the same value of x, increment
it, and write it back, leading to lost updates.
■​ Use Cases:
■​ Flags: A common use case is for boolean flags that signal when a
thread should stop.
■​ Double-Checked Locking (with caveats): volatile can help make
double-checked locking for lazy initialization thread-safe (but requires
careful implementation).
○​ Java Example:​
class SharedFlag {​
private volatile boolean running = true; // volatile boolean flag​

public void stop() {​
running = false;​
}​

public void run() {​
while (running) {​
// Do some work​
}​
[Link]("Thread stopped");​
}​

public static void main(String[] args) throws InterruptedException {​
SharedFlag sharedFlag = new SharedFlag();​
Thread thread = new Thread(sharedFlag::run);​
[Link]();​
[Link](1000);​
[Link](); // Changes the flag​
[Link]();​
}​
}​

2.​ synchronized
○​ Explanation:
■​ synchronized provides a basic locking mechanism in Java. It ensures that
only one thread can execute a block of code or a method at a time, thus
providing mutual exclusion.
■​ It also guarantees visibility. When a thread exits a synchronized block, all
changes made to shared variables within that block are flushed to main
memory. When another thread enters a synchronized block on the same
object, it will see those changes.
■​ synchronized is reentrant. If a thread already holds the lock on an object,
it can acquire the lock again on the same object. This prevents a thread
from blocking itself.
○​ Java Example:​
class Counter {​
private int count = 0;​

public synchronized void increment() { // synchronized method​
count++;​
}​

public int getCount() {​
return count;​
}​

public static void main(String[] args) throws InterruptedException {​
Counter counter = new Counter();​
Thread t1 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​
[Link]();​
}​
});​
Thread t2 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​
[Link]();​
}​
});​
[Link]();​
[Link]();​
[Link]();​
[Link]();​
[Link]("Count: " + [Link]()); // Should be 2000​
}​
}​

3.​ ReentrantLock
○​ Explanation:
■​ ReentrantLock is a more flexible and powerful locking mechanism than
synchronized. It provides the same basic functionality (mutual exclusion
and visibility) but with additional features.
■​ Features:
■​ Fairness: Can be configured to be fair (threads acquire the lock in the
order they requested it) or unfair (more performant, but might lead to
starvation).
■​ Interruptible Locking: Threads waiting to acquire a ReentrantLock
can be interrupted.
■​ Timed Locking: Allows attempts to acquire the lock with a timeout.
■​ Condition Objects: Supports Condition objects, which provide more
sophisticated waiting and notification than wait()/notify().
○​ Java Example:​
import [Link];​
import [Link];​

class MessageQueue {​
private final ReentrantLock lock = new ReentrantLock();​
private final Condition notEmpty = [Link]();​
private String message;​

public void sendMessage(String msg) throws InterruptedException {​
[Link]();​
try {​
while (message != null) {​
[Link](); // Wait if queue is full​
}​
message = msg;​
[Link](); // Notify waiting consumer​
} finally {​
[Link]();​
}​
}​

public String receiveMessage() throws InterruptedException {​
[Link]();​
try {​
while (message == null) {​
[Link](); // Wait if queue is empty​
}​
String receivedMessage = message;​
message = null;​
[Link](); // Notify waiting producer​
return receivedMessage;​
} finally {​
[Link]();​
}​
}​
}​

4.​ Condition
○​ Explanation:
■​ Condition objects provide a way for threads to wait for specific conditions
to become true. They are used in conjunction with ReentrantLock.
■​ Instead of using wait() and notify(), which are tied to an object's intrinsic
monitor, Condition objects allow for more fine-grained control over
waiting and notification.
■​ Key Methods:
■​ await(): Releases the lock and suspends the thread until another
thread signals the condition.
■​ signal(): Wakes up a single thread waiting on the condition.
■​ signalAll(): Wakes up all threads waiting on the condition.
○​ Java Example: (See the MessageQueue example above)
5.​ CountDownLatch
○​ Explanation:
■​ CountDownLatch is a synchronization aid that allows one or more threads
to wait until a set of operations being performed in other threads
completes.
■​ It works by maintaining a count. Each time an operation completes, a
thread calls countDown(), which decrements the count. Threads waiting
on the latch call await(), which blocks until the count reaches zero.
■​ Once the count reaches zero, all waiting threads are released, and the
latch cannot be reset.
○​ Java Example:​
import [Link];​

class Worker implements Runnable {​
private final CountDownLatch latch;​
private final String name;​

public Worker(CountDownLatch latch, String name) {​
[Link] = latch;​
[Link] = name;​
}​

@Override​
public void run() {​
[Link](name + " is working...");​
try {​
[Link](1000); // Simulate work​
} catch (InterruptedException e) {​
[Link]().interrupt();​
}​
[Link](name + " finished.");​
[Link](); // Decrement the count​
}​

public static void main(String[] args) throws InterruptedException {​
CountDownLatch latch = new CountDownLatch(3); // Initialize with 3​
Thread worker1 = new Thread(new Worker(latch, "Worker-1"));​
Thread worker2 = new Thread(new Worker(latch, "Worker-2"));​
Thread worker3 = new Thread(new Worker(latch, "Worker-3"));​

[Link]();​
[Link]();​
[Link]();​

[Link](); // Main thread waits here​
[Link]("All workers have finished. Main thread proceeding.");​
}​
}​

6.​ CyclicBarrier
○​ Explanation:
■​ CyclicBarrier is a synchronization aid that allows a group of threads to
wait for each other to reach a common barrier point. It's similar to
CountDownLatch, but it's reusable.
■​ When a thread reaches the barrier, it calls await(). The thread is blocked
until all other threads in the group have also called await(). Once all
threads have reached the barrier, they are all released, and the barrier
can be reset for another cycle.
■​ Optionally, a Runnable command can be provided to the CyclicBarrier
constructor. This command is executed by the last thread that arrives at
the barrier, before the other threads are released.
○​ Java Example:​
import [Link];​
import [Link];​

class Worker implements Runnable {​
private final CyclicBarrier barrier;​
private final String name;​

public Worker(CyclicBarrier barrier, String name) {​
[Link] = barrier;​
[Link] = name;​
}​

@Override​
public void run() {​
[Link](name + " is doing part 1...");​
try {​
[Link](1000); // Simulate part 1​
[Link](name + " reached the barrier.");​
[Link](); // Wait for others​
[Link](name + " is doing part 2..."); // All proceed together​
[Link](500); // Simulate part 2​
} catch (InterruptedException | BrokenBarrierException e) {​
[Link]().interrupt();​
}​
[Link](name + " finished.");​
}​

public static void main(String[] args) {​
CyclicBarrier barrier = new CyclicBarrier(3, () -> {​
[Link]("All workers have reached the barrier. Proceeding
with next phase.");​
}); // Barrier with 3 parties and a command​
Thread worker1 = new Thread(new Worker(barrier, "Worker-1"));​
Thread worker2 = new Thread(new Worker(barrier, "Worker-2"));​
Thread worker3 = new Thread(new Worker(barrier, "Worker-3"));​

[Link]();​
[Link]();​
[Link]();​
}​
}​

7.​ Semaphore
○​ Explanation:
■​ Semaphore controls access to a limited number of resources. It maintains
a count of available permits.
■​ To access a resource, a thread must acquire a permit by calling acquire().
If the count is greater than zero, the thread gets a permit, and the count is
decremented. If the count is zero, the thread blocks until a permit
becomes available.
■​ When a thread is done with the resource, it releases the permit by calling
release(), which increments the count, potentially waking up a waiting
thread.
○​ Java Example:​
import [Link];​

class ConnectionPool {​
private final Semaphore semaphore;​
private final String[] connections; // Simulate connections​
private final boolean[] used;​

public ConnectionPool(int poolSize) {​
[Link] = new Semaphore(poolSize);​
[Link] = new String[poolSize];​
[Link] = new boolean[poolSize];​
for (int i = 0; i < poolSize; i++) {​
connections[i] = "Connection-" + i;​
}​
}​

public String getConnection() throws InterruptedException {​
[Link](); // Acquire a permit (wait if none available)​
for (int i = 0; i < [Link]; i++) {​
if (!used[i]) {​
used[i] = true;​
return connections[i];​
}​
}​
throw new IllegalStateException("No available connections!"); // Should
not happen​
}​

public void releaseConnection(String connection) {​
for (int i = 0; i < [Link]; i++) {​
if (connections[i].equals(connection)) {​
used[i] = false;​
[Link](); // Release the permit​
return;​
}​
}​
throw new IllegalArgumentException("Invalid connection");​
}​

public static void main(String[] args) throws InterruptedException {​
ConnectionPool pool = new ConnectionPool(3); // Pool of 3 connections​

// Simulate 5 threads trying to get connections​
for (int i = 0; i < 5; i++) {​
final int threadId = i + 1;​
new Thread(() -> {​
String conn = null;​
try {​
conn = [Link]();​
[Link]("Thread " + threadId + " got connection: " +
conn);​
[Link](2000); // Use the connection​
} catch (InterruptedException e) {​
[Link]().interrupt();​
} finally {​
if (conn != null) {​
[Link](conn);​
[Link]("Thread " + threadId + " released connection:
" + conn);​
}​
}​
}).start();​
}​
}​
}​

8.​ [Link]() / unpark()


○​ Explanation:
■​ LockSupport provides very low-level blocking and unblocking primitives
for threads. It's the foundation upon which many higher-level concurrency
utilities are built.
■​ park() blocks the current thread unless a permit is available. The permit is
not tied to any specific object, unlike wait().
■​ unpark(Thread thread) makes the permit available for the given thread,
potentially waking it up if it's parked.
■​ Unlike wait()/notify(), park()/unpark() do not require holding a lock. This
makes them more flexible but also more dangerous if used incorrectly.
○​ Java Example:​
import [Link];​

class ParkExample {​
private static Thread toUnpark;​

public static void main(String[] args) throws InterruptedException {​
Thread parker = new Thread(() -> {​
toUnpark = [Link]();​
[Link]("Parker thread is going to park.");​
[Link](); // Block until unparked​
[Link]("Parker thread is unparked.");​
}, "Parker");​

[Link]();​
[Link](2000); // Give parker time to park​
[Link]("Main thread is going to unpark the parker.");​
[Link](parker); // Unpark the parker thread​
[Link]();​
}​
}​

🔹 Java Memory Model (JMM)


* Explanation:
* The Java Memory Model (JMM) defines how Java threads interact with memory. It specifies
how and when changes made by one thread to shared variables are visible to other threads.
* Key Concepts:
* Working Memory: Each thread has its own working memory, which is an abstraction of the
CPU cache and registers. Threads operate on variables in their working memory.
* Main Memory: Shared variables reside in main memory, which is accessible to all threads.
* Happens-Before Relationship: This is the most crucial concept. It establishes a partial
ordering of memory operations. If operation A "happens-before" operation B, then the results
of A are guaranteed to be visible to B.
* Examples of Happens-Before:
* A thread's start() method happens-before any action in the thread.
* A thread's termination happens-before another thread's join() on that thread.
* A write to a volatile variable happens-before any subsequent read of that variable.
* An unlock on a monitor happens-before a subsequent lock on the same monitor.
* Importance: The JMM is essential for understanding why synchronization is necessary and
how constructs like volatile and synchronized work. Without the JMM, it would be impossible

🔹 Common Problems with Java Concurrency


to reason about the behavior of multithreaded programs.

1.​ ABA Problem


○​ Explanation:
■​ The ABA problem occurs when using Compare-And-Swap (CAS)
operations. CAS is an atomic operation that compares the value of a
variable to an expected value and, if they match, sets the variable to a
new value.
■​ Problem: Suppose a thread reads a value A from a variable. Then, another
thread changes the value to B and then back to A. When the first thread
performs its CAS operation, it sees that the value is still A, and the CAS
succeeds, even though the variable has been modified in the meantime.
This can lead to unexpected behavior in some algorithms.
○​ Java Example:​
import [Link];​

class ABADemo {​
public static void main(String[] args) throws InterruptedException {​
AtomicInteger value = new AtomicInteger(10);​

Thread t1 = new Thread(() -> {​
int expectedValue = [Link]();​
try {​
[Link](100); // Simulate some work​
} catch (InterruptedException e) {​
[Link]().interrupt();​
}​
boolean success = [Link](expectedValue, 20);​
[Link]("Thread 1: CAS success? " + success + ", value: " +
[Link]());​
});​

Thread t2 = new Thread(() -> {​
[Link](15); // A -> B​
[Link](10); // B -> A (ABA)​
[Link]("Thread 2: Value changed to 10");​
});​

[Link]();​
[Link]();​
[Link]();​
[Link]();​
}​
}​

○​ Solution:
■​ Use AtomicStampedReference or AtomicMarkableReference. These
classes associate a version number (stamp) or a boolean mark with the
variable, respectively. The CAS operation then also checks the stamp or
mark, ensuring that the value has not been changed, even if it has the
same value.
2.​ Spurious Wakeups
○​ Explanation:
■​ Spurious wakeups occur when a thread is woken up from a wait() (or
[Link]()) even though the condition it was waiting for is not yet
true.
■​ This is a rare but possible phenomenon in the JVM.
○​ Solution:
■​ Always use a loop to check the condition after a wait() or
[Link]().
synchronized (obj) {​
while (!condition) { // Use a while loop​
[Link]();​
}​
// Condition is now guaranteed to be true​
}​

3.​ Thread Interruption


○​ Explanation:
■​ Java provides a mechanism for interrupting threads. When a thread is
interrupted, its interrupt status is set.
■​ If a thread is blocked in a wait(), join(), or sleep() method, or a
[Link]() call, it will throw an InterruptedException when
interrupted.
■​ It's important to handle InterruptedException properly:
■​ Clean up any resources.
■​ Restore the interrupted status of the thread by calling
[Link]().interrupt(). This ensures that the interruption
is propagated to higher levels of the code.
○​ Java Example:​
class InterruptibleThread extends Thread {​
@Override​
public void run() {​
try {​
[Link]("Thread started. Going to sleep.");​
[Link](5000); // Simulate a long operation​
[Link]("Thread woke up."); // This might not be reached​
} catch (InterruptedException e) {​
[Link]("Thread interrupted!");​
[Link]().interrupt(); // Restore interrupt status​
// Handle interruption (cleanup, etc.)​
}​
[Link]("Thread finished.");​
}​

public static void main(String[] args) throws InterruptedException {​
InterruptibleThread thread = new InterruptibleThread();​
[Link]();​
[Link](1000);​
[Link]("Main thread is interrupting the other thread.");​
[Link](); // Interrupt the thread​
[Link]();​
[Link]("Main thread finished.");​
}​
}​

🔹 Exception Handling in Multithreaded Code


* Approach:
* Catch exceptions in child threads: Exceptions that occur in a thread's run() method are not
automatically propagated to the thread that started it. You must catch them within the run()
method.
* Use [Link]: This interface allows you to define a handler that
will be invoked when a thread terminates due to an uncaught exception. You can set a default
handler for all threads or a specific handler for a particular thread.
* Java Example:
```java​
class MyThread implements Runnable {​
@Override​
public void run() {​
try {​
throw new RuntimeException("Exception from MyThread");​
} catch (RuntimeException e) {​
[Link]("Caught exception in MyThread: " + [Link]());​
}​
}​
}​

class UncaughtExceptionHandlerDemo {​
public static void main(String[] args) {​
// Set a default uncaught exception handler​
[Link]((t, e) -> {​
[Link]("Thread " + [Link]() + " threw an uncaught exception:
" + [Link]());​
});​

Thread thread1 = new Thread(new MyThread(), "Thread-1");​
Thread thread2 = new Thread(() -> {​
throw new RuntimeException("Exception from Thread-2"); // Uncaught​
}, "Thread-2");​

[Link]();​
[Link](); // This exception will be caught by the default handler​
}​
}​
```​

🔹 Advanced Tools
1.​ VarHandle
○​ Explanation:
■​ VarHandle (introduced in Java 9) provides a more powerful and flexible
way to perform low-level operations on variables than AtomicInteger or
volatile. It offers a standardized way to perform memory operations with
various memory ordering guarantees.
■​ It can be used to achieve the same effects as volatile, AtomicInteger, and
even more, with fine-grained control over memory access modes (e.g.,
getVolatile, setOpaque, compareAndSet).
○​ Java Example:​
import [Link];​
import [Link];​

class MyClass {​
public int myField = 0;​
public volatile int myVolatileField = 0;​
}​

class VarHandleDemo {​
public static void main(String[] args) throws NoSuchFieldException,
IllegalAccessException {​
MyClass obj = new MyClass();​
VarHandle handle = [Link]()​
.in([Link])​
.findVarHandle([Link], "myField", [Link]);​

VarHandle volatileHandle = [Link]()​
.in([Link])​
.findVarHandle([Link], "myVolatileField", [Link]);​

// Get and set using VarHandle​
int value = (int) [Link](obj); // Simple get​
[Link]("Initial value of myField: " + value);​
[Link](obj, 10); // Simple set​
[Link]("Value of myField after set: " + [Link](obj));​

// Volatile get and set​
[Link](obj, 25);​
[Link]("Value of myVolatileField: " +
[Link](obj));​

//Atomic CAS​
boolean casSuccess = [Link](obj, 10, 20);​
[Link]("CAS success: " + casSuccess + ", New value of
myField: " + [Link](obj));​

}​
}​

2.​ Unsafe
○​ Explanation:
■​ The Unsafe class provides very low-level, unsafe operations, such as
direct memory access, and is used internally by the JDK.
■​ It should not be used in application-level code because it can easily lead
to memory corruption, JVM crashes, and other unpredictable behavior. It
breaks Java's safety guarantees.
○​ Why to Avoid: Using Unsafe makes your code highly platform-dependent,
difficult to debug, and potentially insecure. Prefer using VarHandle or other
higher-level concurrency constructs.
🔹 Java Bitwise Operators
* Explanation:
* Java provides several bitwise operators that operate on the individual bits of integer values.
* Operators:
* >> Signed right shift: Shifts bits to the right, preserving the sign bit (arithmetic shift).
* >>> Unsigned right shift: Shifts bits to the right, filling the leftmost bits with zeros (logical
shift).
* << Left shift: Shifts bits to the left, filling the rightmost bits with zeros.
* & Bitwise AND
* | Bitwise OR
* ^ Bitwise XOR
* ~ Bitwise complement (unary)
* Java Example: (From your provided text)
```java​
int x = -8; // binary: 11111000​
[Link](x >> 2); // arithmetic shift (preserves sign) Output: -2​
[Link](x >>> 2); // logical shift (fills 0s) Output: 1073741822​
[Link](x << 2); // Left shift Output: -32​
```​

🔹 System Design and Coding Problems (Rubrik Level)


Here, I'll provide more detailed explanations and Java implementations for the system
design and coding problems, expanding on the original guide.
1.​ Concurrent LRU Cache
○​ Explanation:
■​ Implement a Least Recently Used (LRU) cache that is thread-safe. An LRU
cache evicts the least recently accessed element when the cache is full.
■​ Data Structures:
■​ ConcurrentHashMap for thread-safe access to the cache.
■​ LinkedBlockingDeque to maintain the order of recently accessed
elements. A LinkedBlockingDeque allows for efficient removal from
both ends and is thread-safe.
■​ Operations:
■​ get(key):
■​ If the key exists, retrieve the value from the ConcurrentHashMap.
■​ Move the key to the tail of the LinkedBlockingDeque to mark it as
recently used.
■​ If the key doesn't exist, return null.
■​ put(key, value):
■​ If the key exists, update the value in the ConcurrentHashMap and
move the key to the tail of the LinkedBlockingDeque.
■​ If the key doesn't exist:
■​ If the cache is full, remove the head (LRU) element from the
LinkedBlockingDeque and remove its corresponding entry from
the ConcurrentHashMap.
■​ Add the new key-value pair to the ConcurrentHashMap and
the key to the tail of the LinkedBlockingDeque.
○​ Java Implementation:​
import [Link];​
import [Link];​

class ConcurrentLRUCache<K, V> {​
private final int capacity;​
private final ConcurrentHashMap<K, V> map;​
private final LinkedBlockingDeque<K> queue;​

public ConcurrentLRUCache(int capacity) {​
[Link] = capacity;​
[Link] = new ConcurrentHashMap<>(capacity);​
[Link] = new LinkedBlockingDeque<>(capacity);​
}​

public V get(K key) {​
V value = [Link](key);​
if (value != null) {​
// Move key to the tail of the queue​
[Link](key);​
[Link](key);​
return value;​
}​
return null;​
}​

public void put(K key, V value) {​
if ([Link](key)) {​
[Link](key, value);​
[Link](key);​
[Link](key);​
} else {​
if ([Link]() >= capacity) {​
K lruKey = [Link](); // Remove from head​
[Link](lruKey);​
}​
[Link](key, value);​
[Link](key);​
}​
}​

public int size() {​
return [Link]();​
}​

// For testing​
public static void main(String[] args) {​
ConcurrentLRUCache<Integer, String> cache = new
ConcurrentLRUCache<>(3);​
[Link](1, "A");​
[Link](2, "B");​
[Link](3, "C");​
[Link]("Cache: " + [Link] + ", Queue: " + [Link]); //
{1=A, 2=B, 3=C}, [1, 2, 3]​

[Link]("Get 2: " + [Link](2)); // B​
[Link]("Cache: " + [Link] + ", Queue: " + [Link]); //
{1=A, 2=B, 3=C}, [1, 3, 2]​

[Link](4, "D");​
[Link]("Cache: " + [Link] + ", Queue: " + [Link]); //
{2=B, 3=C, 4=D}, [3, 2, 4]​

[Link]("Get 1: " + [Link](1)); // null​
[Link]("Cache: " + [Link] + ", Queue: " + [Link]); //
{2=B, 3=C, 4=D}, [3, 2, 4]​
}​
}​

2.​ Concurrent Map (SDE-2 level)


○​ Explanation:
■​ Design a thread-safe map data structure. The goal is to achieve high
concurrency for both read and write operations.
■​ Approach: A common approach is to use a technique called
segmentation. The map is divided into multiple segments, each of which is
protected by its own lock. This allows multiple threads to access different
segments concurrently.
■​ Data Structures:
■​ An array of segments, where each segment is essentially a smaller
map (e.g., a hash table).
■​ Each segment has its own lock (e.g., a ReentrantLock).
■​ Operations:
■​ get(key):
■​ Determine the segment that the key belongs to.
■​ Acquire the lock for that segment.
■​ Perform the get operation within the segment.
■​ Release the lock.
■​ put(key, value):
■​ Determine the segment that the key belongs to.
■​ Acquire the lock for that segment.
■​ Perform the put operation within the segment.
■​ Release the lock.
■​ remove(key): Similar to put.
○​ Java Implementation:​
import [Link];​
import [Link];​
import [Link];​
import [Link];​

class ConcurrentMap<K, V> {​
private static final int DEFAULT_SEGMENT_SIZE = 16; // Number of segments​
private final Segment<K, V>[] segments;​
private final int segmentMask;​
private final int segmentShift;​

// Inner class representing a segment​
static class Segment<K, V> extends ReentrantLock {​
private final Map<K, V> map = new HashMap<>();​
private final AtomicInteger size = new AtomicInteger(0);​

public V get(K key) {​
try {​
lock();​
return [Link](key);​
} finally {​
unlock();​
}​
}​

public V put(K key, V value) {​
try {​
lock();​
V oldValue = [Link](key, value);​
if (oldValue == null) {​
[Link]();​
}​
return oldValue;​
} finally {​
unlock();​
}​
}​

public V remove(K key) {​
try {​
lock();​
V oldValue = [Link](key);​
if (oldValue != null) {​
[Link]();​
}​
return oldValue;​
} finally {​
unlock();​
}​
}​

public int size() {​
return [Link]();​
}​
}​

// Constructor​
@SuppressWarnings("unchecked")​
public ConcurrentMap() {​
this(DEFAULT_SEGMENT_SIZE);​
}​

@SuppressWarnings("unchecked")​
public ConcurrentMap(int segmentSize) {​
[Link] = new Segment[segmentSize];​
for (int i = 0; i < segmentSize; i++) {​
[Link][i] = new Segment<>();​
}​
[Link] = segmentSize - 1;​
[Link] = 32 - [Link](segmentSize -
1);​
}​

// Helper method to get the segment for a given key​
private Segment<K, V> getSegment(K key) {​
int hash = [Link]();​
int index = (hash >>> segmentShift) & segmentMask;​
return segments[index];​
}​

public V get(K key) {​
return getSegment(key).get(key);​
}​

public V put(K key, V value) {​
return getSegment(key).put(key, value);​
}​

public V remove(K key) {​
return getSegment(key).remove(key);​
}​

public int size() {​
int totalSize = 0;​
for (Segment<K, V> segment : segments) {​
totalSize += [Link]();​
}​
return totalSize;​
}​

public static void main(String[] args) throws InterruptedException {​
ConcurrentMap<String, Integer> map = new ConcurrentMap<>();​
// Use multiple threads to simulate concurrent access​
Thread t1 = new Thread(() -> {​
for (int i = 0; i < 1000; i++) {​
[Link]("key-" + i, i);​
}​
});​
Thread t2 = new Thread(() -> {​
for (int i = 1000; i < 2000; i++) {​
[Link]("key-" + i, i);​
}​
});​
[Link]();​
[Link]();​
[Link]();​
[Link]();​
[Link]("Map size: " + [Link]()); // Should be 2000​
[Link]("Get key-500: " + [Link]("key-500"));​
[Link]("Remove key-1500: " + [Link]("key-1500"));​
[Link]("Map size after removal: " + [Link]()); // Should be
1999​
}​
}​

3.​ Multi-threaded Task Execution System


○​ Explanation:
■​ Design a system to execute tasks concurrently, allowing for control over
the number of threads and how tasks are submitted.
■​ Components:
■​ Thread Pool: A group of worker threads that execute the tasks. Use
ThreadPoolExecutor for a highly configurable thread pool.
■​ Task Queue: A queue to hold the tasks that are waiting to be
executed. Use a BlockingQueue (e.g., LinkedBlockingQueue) for
thread-safe task management.
■​ Task Submission: A mechanism to submit tasks to the system. Use
the [Link]() or [Link]() methods.
○​ Java Implementation:​
import [Link].*;​

class Task implements Runnable {​
private final String name;​

public Task(String name) {​
[Link] = name;​
}​

@Override​
public void run() {​
[Link]("Task " + name + " is being executed by " +
[Link]().getName());​
try {​
[Link](1000); // Simulate task execution​
} catch (InterruptedException e) {​
[Link]().interrupt();​
}​
[Link]("Task " + name + " has completed.");​
}​
}​

class TaskExecutionSystem {​
private final ThreadPoolExecutor executor;​
private final BlockingQueue<Runnable> taskQueue;​

public TaskExecutionSystem(int poolSize, int queueCapacity) {​
[Link] = new LinkedBlockingQueue<>(queueCapacity);​
[Link] = new ThreadPoolExecutor(​
poolSize, // corePoolSize​
poolSize, // maxPoolSize​
60L, // keepAliveTime​
[Link], // keepAliveTime unit​
taskQueue // workQueue​
);​
// Optional: Set a RejectedExecutionHandler​
[Link](new
[Link]()); //runs the task in the thread that calls
execute​
}​

public void submitTask(Runnable task) {​
[Link](task); // Submit the task​
}​

public void shutdown() {​
[Link](); // Initiate shutdown​
try {​
[Link](10, [Link]); // Wait for
termination​
} catch (InterruptedException e) {​
[Link]().interrupt();​
}​
}​

public static void main(String[] args) throws InterruptedException {​
TaskExecutionSystem system = new TaskExecutionSystem(3, 5); // Pool
size 3, queue capacity 5​

// Submit tasks​
for (int i = 0; i < 10; i++) {​
[Link](new Task("Task-" + (i + 1))); //10 tasks​
}​
[Link](8000);​
[Link](); // Shut down the system​
}​
}​

4.​ Bathroom Problem (Democrat / Republican with fairness)


○​ Explanation:
■​ Simulate a bathroom with limited capacity, where Democrats and
Republicans want to use it. The rules are:
■​ Only one party can be in the bathroom at a time.
■​ There must be fairness (prevent starvation).
■​ Approach:
■​ Use a lock (ReentrantLock with fairness) to control access to the
bathroom.
■​ Use conditions (Condition) to allow threads to wait.
■​ Maintain state variables to track the current party in the bathroom and
the number of people inside.
○​ Java Implementation:​
import [Link];​
import [Link];​

class Bathroom {​
private final ReentrantLock lock = new ReentrantLock(true); // Fair lock​
private final Condition democratTurn = [Link]();​
private final Condition republicanTurn = [Link]();​
private int democratsInside = 0;​
private int republicansInside = 0;​
private Party currentParty = null; // null for empty, Democrat, Republican​
private int waitingDemocrat = 0;​
private int waitingRepublican = 0;​
private final int capacity;​

enum Party {​
Democrat, Republican​
}​

public Bathroom(int capacity) {​
[Link] = capacity;​
}​

public void enterDemocrat() throws InterruptedException {​
[Link]();​
waitingDemocrat++;​
try {​
while ((republicansInside > 0 || (currentParty == [Link] &&
democratsInside == 0)) || democratsInside == capacity) {​
[Link]();​
}​
democratsInside++;​
currentParty = [Link];​
waitingDemocrat--;​
} finally {​
[Link]();​
}​
}​

public void enterRepublican() throws InterruptedException {​
[Link]();​
waitingRepublican++;​
try {​
while ((democratsInside > 0 || (currentParty == [Link] &&
republicansInside == 0)) || republicansInside == capacity) {​
[Link]();​
}​
republicansInside++;​
currentParty = [Link];​
waitingRepublican--;​
} finally {​
[Link]();​
}​
}​

public void leaveDemocrat() {​
[Link]();​
try {​
democratsInside--;​
if (democratsInside == 0) {​
if(waitingRepublican > 0){​
[Link]();​
}​
else{​
[Link]();​
}​

}​
} finally {​
[Link]();​
}​
}​

public void leaveRepublican() {​
[Link]();​
try {​
republicansInside--;​
if (republicansInside == 0) {​
if(waitingDemocrat > 0){​
[Link]();​
}​
else{​
[Link]();​
}​
}​
} finally {​
[Link]();​
}​
}​
}​

5.​ Rate Limiter: Leaky Bucket


○​ (See the "Leaky Bucket Implementation" you provided. It's generally
well-structured. Add more comments to explain the time window and rate.)
6.​ Rate Limiter: Sliding Window / Token Bucket
○​ Explanation:
■​ Implement rate limiting using the Sliding Window or Token Bucket
algorithm.
■​ Sliding Window:
■​ Divide time into fixed-size windows.
■​ Keep track of the number of requests in the current window.
■​ When a new request comes in, check if the count exceeds the limit.
■​ Also, consider the previous window's count to smooth out the rate
limiting.
■​ Token Bucket:
■​ Maintain a bucket of tokens.
■​ Each incoming request consumes a token.
■​ Tokens are added to the bucket at a fixed rate.
■​ If the bucket is empty, the request is dropped or delayed.
○​ Java Implementation (Sliding Window):​
import [Link];​
import [Link];​

class SlidingWindowRateLimiter {​
private final int maxRequests;​
private final int windowSizeInSeconds;​
private final AtomicInteger currentWindowCount;​
private final AtomicInteger previousWindowCount;​
private final ReentrantLock lock = new ReentrantLock();​
private volatile long previousWindowStart;​

public SlidingWindowRateLimiter(int maxRequests, int
windowSizeInSeconds) {​
[Link] = maxRequests;​
[Link] = windowSizeInSeconds;​
[Link] = new AtomicInteger(0);​
[Link] = new AtomicInteger(0);​
[Link] = [Link]();​
}​

public boolean allowRequest() {​
long now = [Link]();​
long currentWindowStart = now - (now % (windowSizeInSeconds *
1000L));​

[Link]();​
try {​
// Move the window​
if (currentWindowStart > previousWindowStart) {​
previousWindowStart = currentWindowStart;​
[Link]([Link]());​
[Link](0);​
}​

// Calculate the allowed requests based on the sliding window.​
double ratio = (double) (now - currentWindowStart) /
(windowSizeInSeconds * 1000L);​
int allowedRequests = (int) ([Link]() * (1 - ratio) +
[Link]() * ratio);​

if (allowedRequests < maxRequests) {​
[Link]();​
return true;​
}​
return false;​
} finally {​
[Link]();​
}​
}​

public static void main(String[] args) throws InterruptedException {​
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(10, 1);
// 10 requests per second​
for (int i = 0; i < 20; i++) {​
if ([Link]()) {​
[Link]("Request " + (i + 1) + " allowed");​
} else {​
[Link]("Request " + (i + 1) + " blocked");​
}​
[Link](100); // Simulate 10 requests per second​
}​
}​
}​

7.​ Request Tracker Grouped by IP/UserAgent


○​ Explanation:
■​ Design a system to track the number of requests from different IP
addresses or User-Agents.
■​ Data Structure:
■​ A concurrent map (ConcurrentHashMap) where the key is the IP
address or User-Agent, and the value is a counter (e.g., AtomicInteger)
for the number of requests.
■​ Operations:
■​ trackRequest(ipAddress, userAgent):
■​ Combine IP address and User-Agent to create a unique key.
■​ If the key exists in the map, increment the counter.
■​ If the key doesn't exist, add it to the map with a counter initialized
to 1.
■​ getRequestCount(ipAddress, userAgent):
■​ Retrieve the counter for the given key from the map.
○​ Java Implementation:​
import [Link];​
import [Link];​

class RequestTracker {​
private final ConcurrentHashMap<String, AtomicInteger> requestMap = new
ConcurrentHashMap<>();​

public void trackRequest(String ipAddress, String userAgent) {
○​ ​
String key = ipAddress + "-" + userAgent;​
[Link](key, (k, v) -> (v == null) ? new AtomicInteger(1) :
[Link]());​
}​

public int getRequestCount(String ipAddress, String userAgent) {​
String key = ipAddress + "-" + userAgent;​
AtomicInteger count = [Link](key);​
return (count == null) ? 0 : [Link]();​
}​

public static void main(String[] args) throws InterruptedException {​
RequestTracker tracker = new RequestTracker();​
// Simulate multiple requests from different IPs and User-Agents​
Thread t1 = new Thread(() -> {​
for (int i = 0; i < 5; i++) {​
[Link]("[Link]", "Mozilla/5.0");​
}​
for (int i = 0; i < 3; i++) {​
[Link]("[Link]", "Chrome/80.0");​
}​
});​
Thread t2 = new Thread(() -> {​
for (int i = 0; i < 2; i++) {​
[Link]("[Link]", "Mozilla/5.0");​
}​
for (int i = 0; i < 4; i++) {​
[Link]("[Link]", "Safari/13.0");​
}​
});​
[Link]();​
[Link]();​
[Link]();​
[Link]();​

[Link]("Requests from [Link], Mozilla/5.0: " +
[Link]("[Link]", "Mozilla/5.0")); // 7​
[Link]("Requests from [Link], Chrome/80.0: " +
[Link]("[Link]", "Chrome/80.0")); // 3​
[Link]("Requests from [Link], Safari/13.0: " +
[Link]("[Link]", "Safari/13.0")); // 4​
[Link]("Requests from [Link], Unknown: " +
[Link]("[Link]", "Unknown")); // 0​
}​
}​

8.​ Multi-threaded In-Memory Stock Exchange


○​ (See the "Multi-threaded In-Memory Stock Exchange" section you provided. It
describes the order matching engine. Add more comments to the Java code
to explain the buy/sell queue logic and the synchronization.)
9.​ Task Scheduler with Priority and Fairness
○​ Explanation:
■​ Design a task scheduler that executes tasks based on their priority, but
also ensures fairness to prevent starvation of low-priority tasks.
■​ Data Structures:
■​ Multiple priority queues (PriorityBlockingQueue) to hold tasks, one for
each priority level.
■​ A thread pool to execute the tasks.
■​ Scheduling Algorithm:
■​ Use a weighted round-robin approach. Give each priority queue a
weight, representing how many tasks from that queue should be
executed before moving to the next queue.
■​ Maintain a global lock to protect access to the queues and the
scheduling logic.
○​ Java Implementation:​
import [Link].*;​
import [Link];​

class PrioritizedTask implements Runnable, Comparable<PrioritizedTask> {​
private final int priority;​
private final String name;​

public PrioritizedTask(int priority, String name) {​
[Link] = priority;​
[Link] = name;​
}​

@Override​
public void run() {​
[Link]("Task " + name + " with priority " + priority + " is being
executed by " + [Link]().getName());​
try {​
[Link](1000); // Simulate task execution​
} catch (InterruptedException e) {​
[Link]().interrupt();​
}​
[Link]("Task " + name + " with priority " + priority + " has
completed.");​
}​

@Override​
public int compareTo(PrioritizedTask other) {​
return [Link]([Link], [Link]); // Lower number
means higher priority​
}​

@Override​
public String toString() {​
return "Task{" +​
"priority=" + priority +​
", name='" + name + '\'' +​
'}';​
}​
}​

class PriorityFairTaskScheduler {​
private final PriorityBlockingQueue<PrioritizedTask>[] queues;​
private final int[] weights;​
private final ThreadPoolExecutor executor;​
private final ReentrantLock lock = new ReentrantLock();​
private int currentQueueIndex = 0;​
private int tasksFromCurrentQueue = 0;​

You might also like