0% found this document useful (0 votes)
7 views15 pages

Please Make A Atleast 50 Interview Questions For...

Uploaded by

bhumi031811
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)
7 views15 pages

Please Make A Atleast 50 Interview Questions For...

Uploaded by

bhumi031811
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
You are on page 1/ 15

I.

Core Concepts & Fundamentals


1. What's the difference between a Process and a Thread? Why is a thread
called a lightweight process?
A Process is an instance of a program in execution. Each process has its own private
address space, memory, and resources allocated by the operating system.
Inter-process communication is expensive as it typically requires system calls.

A Thread is the smallest unit of execution within a process. Multiple threads can exist
within a single process and share the process's resources like memory (heap) and file
handles. However, each thread has its own program counter, stack, and local
variables. Because threads share the memory space of the parent process,
communication between threads is much faster and less resource-intensive than
inter-process communication. This shared nature and lower overhead are why threads
are often called "lightweight processes."

2. What are the two primary ways to create a thread in Java? Which one is
preferred and why?
1.​ Extending the Thread class: Create a new class that extends java.lang.Thread and
override its run() method.
2.​ Implementing the Runnable interface: Create a class that implements
java.lang.Runnable and pass an instance of this class to the Thread constructor.

Implementing Runnable is almost always preferred for several key reasons:


●​ Java doesn't support multiple inheritance: If your class already extends
another class, you cannot extend Thread. Implementing an interface avoids this
limitation.
●​ Better Object-Oriented Design: It promotes the separation of concerns. The
task (the code in run()) is decoupled from the execution mechanism (the Thread
object). A single Runnable instance can be executed by multiple threads.
●​ Flexibility: It allows for more flexible designs, especially when using the
ExecutorService framework, which works with Runnable and Callable tasks.

3. Explain the lifecycle of a thread in Java.


A thread in Java goes through the following states:
●​ NEW: The thread has been created but has not yet been started by calling start().
●​ RUNNABLE: The thread is eligible to be run by the JVM's thread scheduler. This
state includes both threads that are actively running and those that are ready to
run but waiting for the scheduler to allocate CPU time.
●​ BLOCKED: The thread is waiting to acquire a monitor lock to enter a synchronized
block/method. It moves to the RUNNABLE state once it acquires the lock.
●​ WAITING: The thread is in an indefinite waiting state. It is waiting for another
thread to perform a specific action. A thread enters this state by calling
Object.wait(), Thread.join(), or LockSupport.park(). It returns to RUNNABLE only when
another thread calls Object.notify()/notifyAll() or LockSupport.unpark().
●​ TIMED_WAITING: The thread is in a waiting state for a specified amount of time.
It enters this state by calling Thread.sleep(long), Object.wait(long), Thread.join(long), etc.
The thread returns to RUNNABLE when the timeout expires or it receives a
notification.
●​ TERMINATED: The thread has completed its execution (the run() method has
finished) or has been otherwise terminated. It cannot be restarted.
4. What is the purpose of the start() and run() methods? What happens if you call
run() directly instead of start()?

●​ start(): This method is used to begin the execution of a thread. It registers the
thread with the thread scheduler, allocates necessary system resources, and then
invokes the run() method in a new, separate thread of execution. start() can only be
called once.
●​ run(): This method contains the actual logic or task that the thread is supposed to
execute.
If you call run() directly, it will be executed just like any other normal method call,
within the same thread that made the call. No new thread will be created. The call
will be a simple synchronous method invocation on the current call stack.

5. What is a daemon thread, and what is its primary use case?


A daemon thread is a low-priority thread that runs in the background to provide
services to user threads. The JVM does not wait for daemon threads to finish their
execution before exiting. When all user (non-daemon) threads have completed, the
JVM automatically terminates any remaining daemon threads.

Use Case: Garbage collection is the most classic example. The GC thread runs in the
background to clean up memory but doesn't need to prevent the application from
shutting down. Other examples include background tasks like monitoring, logging, or
caching services.

You can make a thread a daemon by calling thread.setDaemon(true) before calling start().

II. Synchronization, Locks, and Thread Safety


6. What is the difference between synchronized and volatile?
This is a critical question that tests your understanding of the Java Memory Model
(JMM).

Feature synchronized volatile

Purpose Guarantees mutual exclusion Guarantees visibility and


(atomicity) and visibility. ordering.

Mechanism Uses intrinsic locks (monitors). Acts as a memory barrier.


Only one thread can hold the Forces reads from and writes
lock at a time. to main memory, not
thread-local cache.

Atomicity Ensures that a block of code Does not guarantee atomicity


is executed atomically. for compound actions (e.g.,
i++). It only makes single
read/write operations atomic.

Blocking Can cause threads to block Does not cause threads to


while waiting for the lock. block. It's a non-locking
mechanism.

Applicability Can be applied to methods Can only be applied to


and blocks of code. variables.

Performance Higher overhead due to lock Lower overhead than


acquisition/release. synchronized.

When to use which?


●​ Use synchronized when you need to enforce atomic execution of a critical section
(e.g., updating multiple related variables).
●​ Use volatile when you need to ensure the visibility of a single variable's state across
threads, often for simple flags or status indicators (e.g., a volatile boolean stopFlag).
7. Explain the Java Memory Model (JMM). What problems does it solve?
The JMM defines the rules and guarantees for how threads in a Java program interact
through memory. It specifies the "happens-before" relationship, which is a guarantee
that memory writes by one specific statement are visible to another specific
statement.

The JMM solves two main problems in concurrent programming:


1.​ Visibility: In a multi-core architecture, each CPU has its own cache. A thread
running on one core might modify a shared variable in its local cache, but that
change may not be immediately visible to a thread running on another core. The
JMM provides mechanisms (volatile, synchronized) to ensure that changes are
flushed to main memory and become visible to other threads.
2.​ Reordering: To optimize performance, compilers, JIT, and CPUs can reorder
instructions. This is fine for a single thread but can lead to incorrect behavior in a
multithreaded context if not properly managed. The JMM defines when
reordering is permissible and when it is not, providing guarantees that preserve
the logical correctness of a concurrent program.
8. What is a ReentrantLock? How does it differ from the synchronized keyword?
A ReentrantLock is a concrete implementation of the Lock interface, providing a more
flexible and powerful locking mechanism than synchronized.

Key Differences and Advantages of ReentrantLock:


●​ Fairness: You can create a ReentrantLock with an optional fairness policy. A fair lock
grants access to the longest-waiting thread, preventing starvation. synchronized is
unfair by default.
●​ Timed and Interruptible Lock Acquisition: The tryLock() method allows a thread
to attempt to acquire a lock without blocking, or to try for a specific duration.
lockInterruptibly() allows a thread to be interrupted while waiting for a lock.
synchronized blocks indefinitely and cannot be interrupted.
●​ Condition Objects: ReentrantLock can be associated with one or more Condition
objects, which provide more flexible inter-thread communication than
wait()/notify()/notifyAll(). You can have different conditions for different wait-sets.
●​ Explicit Locking: You must explicitly call lock() and unlock() methods, usually in a
try-finally block to ensure the lock is always released. synchronized handles this
automatically.
When to prefer ReentrantLock?
When you need its advanced features like fairness, timed/interruptible locks, or multiple
condition variables. For simple mutual exclusion, synchronized is often more concise and less
error-prone.
9. What is thread safety? Name three ways to achieve it in Java.
Thread safety is the property of an object or code that guarantees it will behave
correctly when accessed by multiple threads concurrently, without any additional
synchronization on the part of the caller.

Three ways to achieve it:


1.​ Synchronization: Using the synchronized keyword or Lock implementations to
control access to shared, mutable state. This ensures that only one thread can
modify the state at a time.
2.​ Using Atomic Variables: The java.util.concurrent.atomic package provides classes
like AtomicInteger, AtomicLong, and AtomicReference. These classes use low-level,
hardware-based compare-and-swap (CAS) operations to perform atomic
updates without using locks, often resulting in better performance under high
contention.
3.​ Using Concurrent Collections: The java.util.concurrent package provides
thread-safe collection classes like ConcurrentHashMap, CopyOnWriteArrayList, and
BlockingQueue. These are designed and optimized for concurrent access. For
example, ConcurrentHashMap allows multiple concurrent reads and a configurable
number of concurrent writes.
10. What is a ThreadLocal variable? Provide a real-world use case.
A ThreadLocal variable provides a way to store data that is local to a specific thread.
Each thread that accesses a ThreadLocal variable has its own independently initialized
copy of the variable. Other threads cannot see or modify this copy.

Use Case:
A classic use case is for storing per-thread context that you don't want to pass through every
method call.
●​ User Authentication: In a web server, you can store the user's security context
(like user ID, session ID) in a ThreadLocal variable when a request comes in. This
makes the context available to all application layers (service, DAO) that handle
that specific request, without needing to add it as a parameter to every method
signature.
●​ Transaction Management: Storing a database connection or transaction context
for the duration of a thread's work.
Important Note: Be careful with ThreadLocal in environments with thread pools. Since
threads are reused, the ThreadLocal value from a previous task can leak to a new task if
not cleaned up properly. Always use a try-finally block and call threadLocal.remove() in the
finally block.

11. Explain the difference between wait(), notify(), and notifyAll(). Why must they be
called from a synchronized context?
These methods are fundamental for inter-thread communication and are defined on
the java.lang.Object class.
●​ wait(): Causes the current thread to release the monitor lock it holds and enter a
waiting state until another thread invokes notify() or notifyAll() on the same object.
●​ notify(): Wakes up a single arbitrary thread that is waiting on this object's monitor.
●​ notifyAll(): Wakes up all threads that are waiting on this object's monitor. The
awakened threads will then compete to acquire the lock.
Why a synchronized context is required:
A thread must own the monitor lock of an object to call wait(), notify(), or notifyAll() on it. This
is to prevent a critical race condition known as a "lost wakeup." If wait() could be called
without holding the lock, a thread might check a condition (e.g., while (queue.isEmpty())),
decide to wait, but before it can call wait(), another thread could produce an item and call
notify(). The notify() call would be lost, and the first thread would then call wait() and could
potentially wait forever. By requiring the lock, the check and the call to wait() become an
atomic operation.
III. Executor Framework & Asynchronous Programming
12. What is the Executor Framework? What are its benefits over manual thread
creation?
The Executor Framework, introduced in Java 5, is a high-level API for managing and
executing asynchronous tasks (instances of Runnable or Callable). It decouples task
submission from the mechanics of how each task will be run, including details of
thread creation, scheduling, and lifecycle management.

Benefits:
●​ Improved Resource Management: It promotes the use of thread pools, which
reuse existing threads to execute tasks. This avoids the significant overhead of
creating a new thread for every task.
●​ Decoupling: It separates the "what" (the task) from the "how" (the execution
policy), making the code cleaner and more flexible.
●​ Lifecycle Management: It provides a clean way to manage the lifecycle of the
execution service, including starting, shutting down gracefully (shutdown()), and
shutting down forcefully (shutdownNow()).
●​ Backpressure Handling: Thread pools with bounded queues naturally handle
backpressure. If tasks are submitted faster than they can be processed, they will
wait in the queue rather than overwhelming the system.
13. Explain the key parameters of a ThreadPoolExecutor constructor.
The ThreadPoolExecutor is the highly configurable workhorse of the Executor Framework.
Its constructor takes several key parameters:
●​ corePoolSize: The number of threads to keep in the pool, even if they are idle.
●​ maximumPoolSize: The maximum number of threads allowed in the pool.
●​ keepAliveTime: When the number of threads is greater than the core size, this is the
maximum time that excess idle threads will wait for new tasks before terminating.
●​ unit: The time unit for the keepAliveTime argument.
●​ workQueue: The queue used to hold tasks before they are executed. This is a
critical parameter (e.g., ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue).
●​ threadFactory: A factory for creating new threads when needed.
●​ rejectedExecutionHandler: A policy for how to handle tasks that are rejected when the
thread pool and the queue are full.
14. What's the difference between submit() and execute() on an ExecutorService?
●​ execute(Runnable command): This method is defined in the Executor interface. It takes
a Runnable and returns void. It's a "fire-and-forget" method. You cannot get a result
from the task, and you cannot easily handle exceptions that occur within the
task's run() method.
●​ submit(...): This method is defined in ExecutorService. It can take a Runnable or a
Callable. It returns a Future object. The Future represents the result of the
asynchronous computation. You can use it to check if the task is complete, wait
for its completion, and retrieve its result (or the exception it threw). This is the
preferred method when you need to manage the task's outcome.
15. What is a Future? What are its limitations?
A Future represents the result of an asynchronous computation. It provides methods
to:
●​ isDone(): Check if the computation is complete.
●​ cancel(): Attempt to cancel the computation.
●​ get(): Wait for the computation to complete and then retrieve its result. This is a
blocking call.
●​ get(long timeout, TimeUnit unit): A blocking call that waits for a specified time.

Limitations:
●​ Blocking Nature: The get() method is blocking. This can negate the benefits of
asynchronous programming if not used carefully, as the calling thread just ends
up waiting.
●​ No Completion Callbacks: You cannot attach a callback function to a Future that
gets executed automatically upon its completion. You have to manually check its
status.
●​ Cannot be Manually Completed: You cannot programmatically complete a Future
with a result. It can only be completed by its asynchronous task.
●​ Limited Composability: It's difficult to chain or combine multiple Future objects in
a non-blocking way. For example, you can't easily say "when this future
completes, use its result to start another asynchronous task."
16. How does CompletableFuture improve upon Future?
CompletableFuture,introduced in Java 8, is a major enhancement over Future. It
addresses all of Future's limitations and is the cornerstone of modern asynchronous
programming in Java.

Key Improvements:
●​ Non-Blocking and Composable: It has a rich, fluent API for composing,
combining, and chaining asynchronous operations without blocking. You can use
methods like thenApply(), thenAccept(), thenCompose(), and thenCombine().
●​ Completion Callbacks: You can attach callback functions that are executed
automatically when the future completes.
●​ Explicit Completion: You can manually complete a CompletableFuture at any time
using the complete() method. This is incredibly useful for adapting callback-based
APIs.
●​ Advanced Exception Handling: It provides explicit methods like exceptionally()
and handle() to manage exceptions that occur during the asynchronous
computation in a clean, non-blocking way.
17. What is the difference between Callable and Runnable?
●​ Runnable: Its run() method returns void and cannot throw a checked exception.
●​ Callable: Its call() method returns a generic value (V) and can throw a checked
exception.
You would use Callable with ExecutorService.submit() when you need your asynchronous
task to return a result or propagate an exception to the calling thread via the Future
object.

IV. Advanced Concurrency Utilities & Collections


18. Explain CountDownLatch and CyclicBarrier. What are the key differences?
Both are synchronization aids that allow one or more threads to wait for other threads
to complete their operations.

CountDownLatch:

●​ Purpose: Allows one or more threads to wait until a set of operations being
performed in other threads completes.
●​ Mechanism: Initialized with a count. Threads call countDown() to decrement the
count. Threads calling await() will block until the count reaches zero.
●​ One-time use: Once the count reaches zero, the latch cannot be reset.

CyclicBarrier:

●​ Purpose: Allows a set of threads to all wait for each other to reach a common
barrier point.
●​ Mechanism: Initialized with a number of parties (threads). Threads call await().
When a thread calls await(), it blocks until all parties have called await().
●​ Reusable: After the waiting threads are released, the barrier can be reset (or
re-used) for the next cycle. It's "cyclic."
●​ Barrier Action: You can optionally provide a Runnable task that gets executed by
the last thread to arrive at the barrier, before the other threads are released.
Key Difference: A CountDownLatch is for waiting for events to complete. A CyclicBarrier is
for waiting for other threads to reach a common point.

19. How does ConcurrentHashMap work internally? How does it achieve better
concurrency than a synchronized HashMap?
A synchronized HashMap (e.g., Collections.synchronizedMap(new HashMap<>())) uses a single
lock for the entire map. This means only one thread can read or write at a time,
creating a major bottleneck.

ConcurrentHashMap uses a more sophisticated locking strategy.


●​ In Java 7: It used a technique called "lock striping" or "segmentation." The map
was divided into a number of segments (default 16), each with its own lock. A
hash-based function determined which segment a key belonged to. This allowed
up to 16 threads to write to the map concurrently, as long as they were writing to
different segments.
●​ In Java 8 and later: The internal implementation was completely revamped for
better performance. It no longer uses segments. Instead, it uses a finer-grained
locking mechanism where locks are applied at the level of the hash bin (the head
node of the linked list or tree in a bucket). When a write operation needs to be
performed on a bucket, it only locks that specific bucket. If there's no contention,
it may use optimistic locking with CAS operations. This allows for a much higher
degree of concurrency. Read operations are generally non-blocking.
20. What is a BlockingQueue? Describe one implementation and its use case.
A BlockingQueue is a queue that supports operations that wait for the queue to become
non-empty when retrieving an element, and wait for space to become available in the
queue when adding an element. This makes it an ideal tool for inter-thread
communication.

Implementation: ArrayBlockingQueue
●​ Mechanism: A fixed-size, bounded blocking queue backed by an array.
●​ Behavior:
○​ put(E e): Blocks if the queue is full until space becomes available.
○​ take(): Blocks if the queue is empty until an item is available.
●​ Use Case: The Producer-Consumer Problem. This is the canonical use case.
○​ Producers create items and put them onto the ArrayBlockingQueue using put(). If
the queue is full, the producer thread will automatically block, preventing the
system from being overwhelmed.
○​ Consumers take items from the queue using take(). If the queue is empty, the
consumer thread will automatically block, waiting for work to do.​
This pattern decouples the producer and consumer and provides natural
backpressure handling.
21. Explain the CopyOnWriteArrayList. When is it a good choice, and what are its
performance characteristics?
CopyOnWriteArrayList is a thread-safe variant of ArrayList in which all mutative operations
(add, set, etc.) are implemented by making a fresh copy of the underlying array.

How it works:
●​ Reads: Read operations are lock-free and operate on an immutable snapshot of
the array at the time the read began. They are very fast.
●​ Writes: When a write operation occurs, a lock is acquired, the entire underlying
array is copied, the modification is made to the new copy, and then the internal
reference is atomically swapped to point to the new array.
When to use it:
It's an excellent choice when you have a collection that is read far more often than it is written
to. For example, a list of listeners or observers in an event-based system. Many threads can
iterate over the list concurrently without any synchronization overhead, while modifications
are rare but still need to be thread-safe.
Performance Characteristics:
●​ Reads/Iteration: Extremely fast, no locking.
●​ Writes: Extremely expensive due to array copying. The cost is proportional to the
size of the list. Not suitable for lists that are frequently modified.
V. Deadlocks, Livelocks, and Tricky Scenarios
22. What is a deadlock? Describe the four conditions necessary for a deadlock
to occur.
A deadlock is a state in which two or more threads are blocked forever, each waiting
for a resource that is held by another thread in the same set.

The four necessary conditions (the Coffman conditions) are:


1.​ Mutual Exclusion: At least one resource must be held in a non-sharable mode.
Only one thread can use the resource at any given time.
2.​ Hold and Wait: A thread must be holding at least one resource and waiting to
acquire additional resources that are currently being held by other threads.
3.​ No Preemption: A resource can only be released voluntarily by the thread
holding it after that thread has completed its task.
4.​ Circular Wait: A set of threads {T0, T1, ..., Tn} must exist such that T0 is waiting
for a resource held by T1, T1 is waiting for a resource held by T2, ..., and Tn is
waiting for a resource held by T0.
23. How can you prevent deadlocks?
You can prevent deadlocks by breaking one of the four necessary conditions.
●​ Break Circular Wait: This is the most common and practical approach. Enforce a
strict ordering for lock acquisition. For example, if you have two locks, L1 and L2,
establish a rule that any thread needing both locks must always acquire L1 before
acquiring L2. This makes a circular wait impossible.
●​ Break Hold and Wait: A thread could try to acquire all its required locks at once.
If it can't get all of them, it releases any it did acquire and tries again later. The
Lock.tryLock() method is useful here.
●​ Break No Preemption: This is often not practical.
●​ Break Mutual Exclusion: This is only possible if the resources can be made
sharable (e.g., by using lock-free data structures).
24. What are livelock and starvation? How do they differ from deadlock?
●​ Livelock: A situation where two or more threads are not blocked, but they are
constantly changing their state in response to each other's actions, without
making any useful progress. A real-world analogy is two people trying to pass in a
narrow hallway; they both step aside, then both step the other way, and so on,
forever. The threads are active and consuming CPU, but getting no work done.
●​ Starvation: A situation where a thread is perpetually denied access to a resource
it needs in order to make progress. This can happen if "greedy" threads
constantly acquire the resource, or if the thread scheduling policy (e.g., thread
priorities) consistently favors other threads. Using fair locks (new
ReentrantLock(true)) is one way to prevent starvation.
Difference from Deadlock: In a deadlock, threads are blocked and waiting. In a
livelock, threads are active but unproductive. In starvation, one or more threads are
blocked from gaining access to a resource, while other threads may be proceeding
just fine.

25. You are using ExecutorService. What is the difference between shutdown() and
shutdownNow()? How do you gracefully shut down an executor?

●​ shutdown(): This is the graceful shutdown method. It stops accepting new tasks,
but it allows all previously submitted tasks (both running and waiting in the queue)
to complete execution.
●​ shutdownNow(): This is the abrupt shutdown method. It attempts to stop all actively
executing tasks by interrupting them (Thread.interrupt()), drains the queue of waiting
tasks, and returns a list of the tasks that were awaiting execution.
Graceful Shutdown Pattern:
The best practice is to use a combination of shutdown() and awaitTermination():
ExecutorService executor = Executors.newFixedThreadPool(10);​
// ... submit tasks​
executor.shutdown(); // Disable new tasks from being submitted​
try {​
// Wait a while for existing tasks to terminate​
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {​
executor.shutdownNow(); // Cancel currently executing tasks​
// Wait a while for tasks to respond to being cancelled​
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {​
System.err.println("Pool did not terminate");​
}​
}​
} catch (InterruptedException ie) {​
// (Re-)Cancel if current thread also interrupted​
executor.shutdownNow();​
// Preserve interrupt status​
Thread.currentThread().interrupt();​
}​

26. Why is it generally a bad idea to use Thread.stop()?


The Thread.stop() method is inherently unsafe and has been deprecated for this reason.
When you call stop(), it throws a ThreadDeath error in the target thread. This can happen
at any point in the thread's execution. If the thread was in the middle of a synchronized
block and holding a lock, that lock is immediately released. This can leave the shared
data protected by that lock in an inconsistent, corrupted state. Other threads might
then view this corrupted data, leading to unpredictable and difficult-to-debug errors.

The correct way to stop a thread is through cooperative interruption, typically using a
volatile boolean flag or, more robustly, the built-in interruption mechanism
(thread.interrupt() and checking Thread.currentThread().isInterrupted()).

VI. Java Multithreading Coding Problems (10-15 Questions)


This section contains practical coding problems that are frequently asked in
interviews for a mid-level developer role.

Problem 1: Implement a Blocking Queue


Write a thread-safe blocking queue with a fixed capacity. It should have put(E item) and take()
methods. put should block if the queue is full, and take should block if the queue is empty.
(Hint: Use ReentrantLock and Condition variables for a robust implementation, or
synchronized with wait()/notifyAll() for a simpler one.)
Problem 2: The Producer-Consumer Problem
Using the BlockingQueue interface (e.g., ArrayBlockingQueue), set up a classic
producer-consumer scenario. One or more producer threads should generate data
(e.g., integers) and put them onto the queue. One or more consumer threads should
take the data from the queue and process it (e.g., print it).

Problem 3: Print "FooBar" Alternately


You are given a class FooBar. You have two threads, one that calls the foo() method and one
that calls the bar() method. Modify the class so that "foobar" is printed n times. The foo()
method should print "foo" and the bar() method should print "bar".
(Hint: Use Semaphore or CountDownLatch to signal between the two threads.)
Problem 4: Print Numbers in Sequence by Three Threads
You have three threads. The first thread should print 1, the second should print 2, and the
third should print 3. Then the first thread should print 4, the second 5, the third 6, and so on,
up to a given limit N.
(Hint: Use a shared volatile variable to track the current number and a remainder/modulo
operation to determine which thread's turn it is. Use synchronized and wait/notifyAll to
coordinate.)
Problem 5: Implement a Thread-Safe Singleton
Implement a singleton pattern that is safe to use in a multithreaded environment.
Discuss the pros and cons of different approaches (e.g., eager initialization,
synchronized getInstance, double-checked locking). Show the modern, preferred way
using an enum or an initialization-on-demand holder class.
Problem 6: Web Crawler
Design and implement a simple web crawler that uses an ExecutorService to fetch and parse
web pages concurrently. The crawler should start with a seed URL, extract all links from that
page, and then submit new tasks to the executor to crawl those links. Be mindful of avoiding
duplicate URLs and managing the crawl depth.
(Hint: Use a ConcurrentHashMap or a ConcurrentSkipListSet to keep track of visited URLs.)
Problem 7: Implement a Simple Rate Limiter
Implement a rate limiter that restricts the number of requests a client can make in a given time
window (e.g., no more than 10 requests per second). The isAllowed() method should be
thread-safe.
(Hint: A "token bucket" algorithm is a common approach. You can implement this using a
BlockingQueue or a combination of a timestamp and a counter.)
Problem 8: Deadlock Detection and Resolution (Scenario)
You are given two classes, Account and Transaction. The Transaction class has a method
transfer(Account from, Account to, int amount) which needs to lock both accounts to perform
the transfer safely. Write code that could potentially lead to a deadlock. Then, explain
how you would fix it by enforcing a lock ordering.

Problem 9: Using CompletableFuture for Asynchronous Tasks


Rewrite a piece of synchronous code that performs three sequential steps (e.g., fetch
user data, fetch user orders, enrich orders with product details) into an asynchronous,
non-blocking version using CompletableFuture. Use methods like thenApplyAsync and
thenCombine.

Problem 10: The Dining Philosophers Problem


Implement a solution to the classic Dining Philosophers problem, where five philosophers sit
at a circular table and alternate between thinking and eating. There are five chopsticks, one
between each pair of philosophers. A philosopher needs two chopsticks to eat. Your solution
must prevent deadlock and starvation.
(Hint: A common solution involves having one philosopher pick up their chopsticks in the
reverse order (e.g., right then left) compared to the others, or using a central arbiter/lock.)
Problem 11: Implement a ReadWriteLock
Explain the concept of a ReadWriteLock. When is it more performant than a standard
ReentrantLock? Write a small piece of code for a thread-safe cache (get and put
methods) that uses a ReentrantReadWriteLock to allow concurrent reads.

Problem 12: Merge K Sorted Lists Concurrently


Given K sorted lists (or arrays), write a function to merge them into a single sorted list. Use a
concurrent approach, for example, by using an ExecutorService to merge pairs of lists in
parallel until only one remains.
(Hint: This is a divide-and-conquer problem that maps well to a ForkJoinPool or a standard
ExecutorService.)
Problem 13: Asynchronous Task with Timeout
Using CompletableFuture, write a function that executes a long-running task but returns a
default value if the task does not complete within a specified timeout (e.g., 2 seconds).
(Hint: Look at the orTimeout() and completeOnTimeout() methods in CompletableFuture.)
Problem 14: Using a Phaser
A Phaser is a more flexible and reusable barrier than CyclicBarrier. Implement a task
where multiple threads perform work in phases. For example, three threads must all
complete Phase 1 before any of them can start Phase 2, and all must complete Phase
2 before they can start Phase 3. Show how a Phaser can coordinate this.

Problem 15: The Unsafe i++


Write a simple program where multiple threads increment a shared integer counter.
Run it and show that the final result is almost always less than the expected value due
to the non-atomic nature of the ++ operation. Then, fix the program using AtomicInteger
and explain why it works. This is a fundamental test of understanding race conditions.

You might also like