What is Multithreading in Java?
Multithreading in Java is a feature that allows concurrent execution of two or more parts of a program to maximize the utilization of CPU. Each part of such a program is called a thread. Threads are lightweight processes within a process, and they share the same memory space, allowing efficient communication and data exchange among them.
Java provides built-in support for multithreading using the java.lang.Thread
class and the java.lang.Runnable
interface. Multithreading is crucial for performing multiple tasks simultaneously, which can significantly improve the performance and responsiveness of applications.
Example Code:
class MultithreadingDemo extends Thread {
public void run() {
try {
for (int i = 0; i < 5; i++) {
System.out.println("Thread " + Thread.currentThread().getId() + " is running");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Thread interrupted.");
}
}
public static void main(String[] args) {
MultithreadingDemo thread1 = new MultithreadingDemo();
thread1.start();
MultithreadingDemo thread2 = new MultithreadingDemo();
thread2.start();
}
}
In this example, two threads are created and started, running concurrently.
What are the Benefits of Using Multithreading in Java?
Multithreading in Java provides several advantages, including:
- Improved Performance: By dividing a task into multiple threads, the overall execution time can be reduced as threads run in parallel.
- Better Resource Utilization: Multithreading makes better use of system resources by keeping the CPU busy.
- Simplified Modeling: It simplifies complex problems by breaking them into smaller, more manageable tasks.
- Enhanced Responsiveness: Multithreading improves the responsiveness of applications, especially GUI applications, by offloading background tasks to separate threads.
- Scalability: Multithreading allows applications to handle a higher number of tasks concurrently.
- Reduced Blocking: It reduces blocking time in applications, making them more efficient.
Example Code:
class Task implements Runnable {
private String taskName;
public Task(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(taskName + " is running.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(taskName + " has finished.");
}
public static void main(String[] args) {
Thread thread1 = new Thread(new Task("Task 1"));
Thread thread2 = new Thread(new Task("Task 2"));
thread1.start();
thread2.start();
}
}
This example demonstrates how multithreading can enhance the responsiveness and performance of an application by running tasks concurrently.
Explain the Difference Between a Process and a Thread.
A process is an instance of a program in execution. It has its own memory space, system resources, and can contain multiple threads. Processes are isolated from each other and have their own address space, which makes inter-process communication (IPC) more complex and slower compared to threads.
A thread, on the other hand, is a smaller unit of a process. Multiple threads within the same process share the same memory space and resources, making communication between them faster and more efficient. However, this also introduces the risk of synchronization issues if threads access shared resources concurrently.
Example Code:
class ProcessDemo {
public static void main(String[] args) {
// Simulating a process with multiple threads
Thread thread1 = new Thread(new Task("Thread 1"));
Thread thread2 = new Thread(new Task("Thread 2"));
thread1.start();
thread2.start();
}
}
class Task implements Runnable {
private String threadName;
public Task(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
System.out.println(threadName + " is running.");
}
}
In this example, Thread 1
and Thread 2
are threads within the same process, sharing the same memory space.
How Do You Create a Thread in Java?
In Java, you can create a thread in two primary ways: by extending the Thread
class or by implementing the Runnable
interface.
- Extending the
Thread
Class:
- Create a new class that extends
Thread
. - Override the
run()
method to define the task that the thread will perform. - Create an instance of the class and call the
start()
method to begin execution.
Example Code:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running by extending Thread class.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- Implementing the
Runnable
Interface:
- Create a new class that implements the
Runnable
interface. - Implement the
run()
method to define the task. - Create an instance of the class and pass it to a
Thread
object. - Call the
start()
method on theThread
object.
Example Code:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running by implementing Runnable interface.");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
Both methods achieve the same result, but implementing Runnable
is generally preferred as it allows your class to extend other classes.
What is the Difference Between Extending the Thread Class and Implementing the Runnable Interface?
The primary difference between extending the Thread
class and implementing the Runnable
interface lies in how they allow for thread creation and the flexibility they provide:
- Extending the
Thread
Class:
- A class that extends
Thread
cannot extend any other class due to Java’s single inheritance model. - It directly inherits all the methods of the
Thread
class. - It is generally used for simple thread creation when no other superclass is needed.
Example Code:
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running by extending Thread class.");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- Implementing the
Runnable
Interface:
- A class that implements
Runnable
can still extend another class. - It separates the task being executed from the thread that executes it, promoting better object-oriented design.
- It is more flexible and is generally preferred for larger applications.
Example Code:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread running by implementing Runnable interface.");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
Using Runnable
is preferred when the task needs to be reused or when the class needs to extend another class.
How Do You Start a Thread in Java?
To start a thread in Java, you must call the start()
method on an instance of Thread
. The start()
method creates a new thread and executes the run()
method of the Thread
or Runnable
object.
Simply calling the run()
method directly does not create a new thread; it will execute the run()
method in the current thread.
Example Code:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running.");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Starts a new thread
}
}
In this example, calling thread.start()
creates a new thread and invokes the run()
method in that new thread.
What is the Lifecycle of a Thread in Java?
The lifecycle of a thread in Java consists of several states:
- New: A thread is in this state when it is created but not yet started. It remains in this state until the
start()
method is called. - Runnable: After the
start()
method is called, the thread moves to the runnable state. The thread is ready to run and is waiting for CPU time. - Running: When the thread scheduler picks the thread, it moves from the runnable state to the running state, and its
run()
method is executed. - Blocked/Waiting: A thread enters this state when it is waiting for a resource or another thread to complete a task. It remains in this state until the resource becomes available or the task is completed.
- Timed Waiting: This state is similar to waiting but occurs for a specified period. A thread can enter this state by calling methods like
sleep(long millis)
orwait(long timeout)
. - Terminated: A thread enters this state when its
run()
method completes or if it is explicitly terminated. Once in this state, the thread cannot be restarted.
Example Code:
class ThreadLifecycleDemo extends Thread {
@Override
public void run() {
System.out.println("Thread is running.");
try
{
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread is terminating.");
}
public static void main(String[] args) {
ThreadLifecycleDemo thread = new ThreadLifecycleDemo();
System.out.println("Thread is in new state.");
thread.start();
System.out.println("Thread is in runnable state.");
}
}
This example illustrates the transition of a thread through different states.
Explain the Thread Class Methods: start(), run(), sleep(), join(), and interrupt().
- start(): This method is used to start a new thread. It causes the JVM to call the
run()
method of the thread.
Thread thread = new Thread(new MyRunnable());
thread.start(); // Starts a new thread
- run(): This method defines the code that constitutes the new thread. It is called when the thread is started.
public void run() {
System.out.println("Thread is running.");
}
- sleep(long millis): This method pauses the execution of the current thread for the specified number of milliseconds.
try {
Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
- join(): This method allows one thread to wait for the completion of another. If
join()
is called on a thread, the calling thread will be blocked until the target thread finishes.
Thread thread = new Thread(new MyRunnable());
thread.start();
try {
thread.join(); // Wait for the thread to finish
} catch (InterruptedException e) {
e.printStackTrace();
}
- interrupt(): This method interrupts a thread, causing it to stop its current task. It does not immediately terminate the thread but sets the interrupt flag, which the thread can check to stop itself.
thread.interrupt(); // Interrupt the thread
Example Code:
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("Thread interrupted.");
}
System.out.println("Thread completed.");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
This example demonstrates the use of various thread methods in Java.
What is a Daemon Thread in Java?
A daemon thread in Java is a background thread that is designed to provide services to user threads. Daemon threads are typically used for tasks such as garbage collection, background computation, and other housekeeping tasks. They run in the background and do not prevent the JVM from exiting when all user threads have finished execution.
The key characteristic of daemon threads is that the JVM does not wait for daemon threads to complete before exiting. If all user threads terminate, the JVM will shut down, and any remaining daemon threads will be stopped abruptly.
Example Code:
class DaemonThreadDemo extends Thread {
@Override
public void run() {
while (true) {
System.out.println("Daemon thread is running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
DaemonThreadDemo daemonThread = new DaemonThreadDemo();
daemonThread.setDaemon(true); // Setting the thread as daemon
daemonThread.start();
try {
Thread.sleep(3000); // Main thread sleeps for 3 seconds
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is terminating.");
}
}
In this example, the daemon thread runs in the background and prints a message every second. When the main thread terminates after 3 seconds, the JVM exits, and the daemon thread is stopped.
How Do You Set a Thread as a Daemon Thread?
To set a thread as a daemon thread, you need to call the setDaemon(true)
method on the Thread
object before starting the thread. Once a thread is started, you cannot change its daemon status.
Example Code:
class DaemonThreadExample extends Thread {
@Override
public void run() {
while (true) {
System.out.println("Daemon thread running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
DaemonThreadExample daemonThread = new DaemonThreadExample();
daemonThread.setDaemon(true); // Setting the thread as a daemon
daemonThread.start();
try {
Thread.sleep(3000); // Main thread sleeps for 3 seconds
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is terminating.");
}
}
In this example, the setDaemon(true)
method sets the daemonThread
as a daemon thread before starting it.
What is Thread Synchronization, and Why is it Important?
Thread synchronization in Java is a mechanism that ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as the critical section. Synchronization is essential to prevent thread interference and consistency problems when multiple threads access shared resources.
Without synchronization, threads can execute critical sections of code simultaneously, leading to data inconsistency, race conditions, and unpredictable behavior. Synchronization ensures that only one thread can access the critical section at a time, maintaining data integrity and consistency.
Example Code:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class SyncThread extends Thread {
private Counter counter;
public SyncThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
public static void main(String[] args) {
Counter counter = new Counter();
SyncThread thread1 = new SyncThread(counter);
SyncThread thread2 = new SyncThread(counter);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the increment()
method is synchronized, ensuring that only one thread can increment the counter at a time, preventing data inconsistency.
What are the Different Ways to Achieve Thread Synchronization in Java?
There are several ways to achieve thread synchronization in Java:
- Synchronized Methods: Methods can be synchronized using the
synchronized
keyword, ensuring that only one thread can execute the method at a time.
public synchronized void increment() {
count++;
}
- Synchronized Blocks: Specific blocks of code can be synchronized, allowing finer-grained control over synchronization.
public void increment() {
synchronized(this) {
count++;
}
}
- Static Synchronization: Static methods can be synchronized to control concurrent access to class-level resources.
public static synchronized void staticIncrement() {
staticCount++;
}
- Using Locks: The
java.util.concurrent.locks.Lock
interface provides more flexible and sophisticated synchronization mechanisms compared to thesynchronized
keyword.
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
- Volatile Keyword: The
volatile
keyword ensures that changes to a variable are always visible to all threads, but it does not provide atomicity.
private volatile boolean flag = true;
Example Code:
class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
class SyncThread extends Thread {
private Counter counter;
public SyncThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
public static void main(String[] args) {
Counter counter = new Counter();
SyncThread thread1 = new SyncThread(counter);
SyncThread thread2 = new SyncThread(counter);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, ReentrantLock
is used for synchronization, providing more flexible control
over the synchronization process.
Explain the Synchronized Keyword in Java.
The synchronized
keyword in Java is used to control access to a block of code or method by multiple threads. When a method or block is synchronized, only one thread can execute it at a time, ensuring that shared resources are accessed in a thread-safe manner.
When a thread enters a synchronized method or block, it acquires a lock associated with the object or class. Other threads trying to enter the synchronized code must wait until the lock is released by the current thread.
Example Code:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
class SyncThread extends Thread {
private Counter counter;
public SyncThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
public static void main(String[] args) {
Counter counter = new Counter();
SyncThread thread1 = new SyncThread(counter);
SyncThread thread2 = new SyncThread(counter);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the increment()
and getCount()
methods are synchronized, ensuring thread-safe access to the count
variable.
What is the Difference Between a Synchronized Method and a Synchronized Block?
The primary difference between a synchronized method and a synchronized block lies in the granularity of synchronization:
- Synchronized Method: The entire method is synchronized, meaning the lock is acquired when the method is called and released when the method returns.
public synchronized void increment() {
count++;
}
- Synchronized Block: Only a specific block of code within a method is synchronized, allowing finer-grained control over the synchronization process. This can lead to better performance by reducing the scope of the lock.
public void increment() {
synchronized(this) {
count++;
}
}
Example Code:
class Counter {
private int count = 0;
public void increment() {
synchronized(this) {
count++;
}
}
public int getCount() {
return count;
}
}
class SyncThread extends Thread {
private Counter counter;
public SyncThread(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
public static void main(String[] args) {
Counter counter = new Counter();
SyncThread thread1 = new SyncThread(counter);
SyncThread thread2 = new SyncThread(counter);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + counter.getCount());
}
}
In this example, only the increment operation is synchronized, allowing other operations to be performed concurrently.
What is a Monitor in Java?
A monitor in Java is a synchronization construct that allows threads to have mutual exclusive access to shared resources. Each object in Java is associated with a monitor, which is used to coordinate access to the object’s methods and fields.
When a thread acquires a lock on an object, it enters the monitor associated with that object. Other threads trying to enter the monitor are blocked until the lock is released.
Example Code:
class MonitorExample {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
class SyncThread extends Thread {
private MonitorExample monitorExample;
public SyncThread(MonitorExample monitorExample) {
this.monitorExample = monitorExample;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
monitorExample.increment();
}
}
public static void main(String[] args) {
MonitorExample monitorExample = new MonitorExample();
SyncThread thread1 = new SyncThread(monitorExample);
SyncThread thread2 = new SyncThread(monitorExample);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + monitorExample.getCount());
}
}
In this example, the MonitorExample
class uses synchronized methods to ensure mutual exclusive access to the count
variable.
How Does the Synchronized Keyword Work Under the Hood?
The synchronized
keyword in Java uses intrinsic locks or monitors to control access to synchronized code. Each object in Java has an associated intrinsic lock. When a thread enters a synchronized method or block, it acquires the lock associated with the object.
Under the hood, the JVM ensures that only one thread can hold the lock at any given time. Other threads attempting to enter the synchronized code are blocked until the lock is released. This mechanism ensures that only one thread can execute the synchronized code, preventing data inconsistency and race conditions.
Example Code:
class SynchronizedExample {
private int count = 0;
public void increment() {
synchronized(this) {
count++;
}
}
public int getCount() {
return count;
}
}
class SyncThread extends Thread {
private SynchronizedExample synchronizedExample;
public SyncThread(SynchronizedExample synchronizedExample) {
this.synchronizedExample = synchronizedExample;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronizedExample.increment();
}
}
public static void main(String[] args) {
SynchronizedExample synchronizedExample = new SynchronizedExample();
SyncThread thread1 = new SyncThread(synchronizedExample);
SyncThread thread2 = new SyncThread(synchronizedExample);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + synchronizedExample.getCount());
}
}
In this example, the synchronized
keyword ensures that only one thread can execute the increment operation at a time, maintaining data consistency.
What is the Difference Between wait(), notify(), and notifyAll() Methods in Java?
The wait()
, notify()
, and notifyAll()
methods are used for inter-thread communication in Java, allowing threads to coordinate their actions.
- wait(): This method causes the current thread to release the lock and enter the waiting state until another thread calls
notify()
ornotifyAll()
on the same object.
synchronized(lock) {
lock.wait();
}
- notify(): This method wakes up a single thread that is waiting on the object’s monitor. If multiple threads are waiting, one is chosen arbitrarily.
synchronized(lock) {
lock.notify();
}
- notifyAll(): This method wakes up all the threads that are waiting on the object’s monitor.
synchronized(lock) {
lock.notifyAll();
}
Example Code:
class WaitNotifyExample {
private final Object lock = new Object();
public void produce() throws InterruptedException {
synchronized (lock) {
System.out.println("Producer is producing...");
lock.wait();
System.out.println("Producer resumed.");
}
}
public void consume() throws InterruptedException {
Thread.sleep(1000);
synchronized (lock) {
System.out.println("Consumer is consuming...");
lock.notify();
}
}
public static void main(String[] args) {
WaitNotifyExample example = new WaitNotifyExample();
Thread producer = new Thread(() -> {
try {
example.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
example.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
}
}
In this example, the produce()
method waits for the lock to be notified, while the consume()
method notifies the lock, allowing the producer to resume.
Explain the Concept of Deadlock in Java. How Can You Avoid It?
A deadlock in Java occurs when two or more threads are blocked forever, waiting for each other to release resources. Deadlock is a serious issue in multithreading and can cause programs to hang indefinitely.
Example of Deadlock:
class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized (lock1) {
System.out.println("Thread 1: Holding lock 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock 2...");
synchronized (lock
2) {
System.out.println("Thread 1: Holding lock 1 & 2...");
}
}
}
public void method2() {
synchronized (lock2) {
System.out.println("Thread 2: Holding lock 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock 1...");
synchronized (lock1) {
System.out.println("Thread 2: Holding lock 2 & 1...");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
Thread thread1 = new Thread(example::method1);
Thread thread2 = new Thread(example::method2);
thread1.start();
thread2.start();
}
}
In this example, thread1
holds lock1
and waits for lock2
, while thread2
holds lock2
and waits for lock1
, causing a deadlock.
Avoiding Deadlock:
- Avoid Nested Locks: Minimize the use of nested locks. If you must use them, always acquire locks in the same order.
synchronized(lock1) {
synchronized(lock2) {
// Critical section
}
}
- Use Try-Lock: Use the
tryLock()
method fromReentrantLock
to attempt to acquire locks without waiting indefinitely.
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
if (lock1.tryLock() && lock2.tryLock()) {
try {
// Critical section
} finally {
lock1.unlock();
lock2.unlock();
}
}
- Timeouts: Use timeouts to acquire locks and avoid waiting forever.
if (lock1.tryLock(1000, TimeUnit.MILLISECONDS) && lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
// Critical section
} finally {
lock1.unlock();
lock2.unlock();
}
}
- Avoid Circular Wait: Design your system to avoid circular wait conditions by ensuring that a thread can hold only one resource at a time.
What is a Race Condition? How Can You Prevent It?
A race condition occurs when multiple threads access shared data simultaneously, and the outcome of the execution depends on the timing of the threads’ execution. Race conditions can lead to inconsistent and unpredictable results.
Example of Race Condition:
class RaceConditionExample {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
public static void main(String[] args) {
RaceConditionExample example = new RaceConditionExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
In this example, thread1
and thread2
increment the count
variable simultaneously, leading to a race condition.
Preventing Race Conditions:
- Synchronization: Use synchronized methods or blocks to ensure that only one thread can access the shared resource at a time.
public synchronized void increment() {
count++;
}
- Locks: Use explicit locks to control access to shared resources.
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
- Atomic Variables: Use atomic variables, such as
AtomicInteger
, which provide thread-safe operations.
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
Example Code:
import java.util.concurrent.atomic.AtomicInteger;
class RaceConditionExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
RaceConditionExample example = new RaceConditionExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
In this example, AtomicInteger
is used to prevent race conditions by ensuring atomicity of the increment operation.
What is the Volatile Keyword in Java? When Would You Use It?
The volatile
keyword in Java is used to indicate that a variable’s value will be modified by different threads. When a variable is declared as volatile, it ensures that its value is always read from and written to the main memory, rather than being cached in the CPU registers or thread-local cache. This guarantees visibility of changes to the variable across all threads.
Example Usage of Volatile:
class VolatileExample {
private volatile boolean flag = true;
public void run() {
while (flag) {
System.out.println("Running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Stopped.");
}
public void stop() {
flag = false;
}
public static void main(String[] args) {
VolatileExample example = new VolatileExample();
Thread thread = new Thread(example::run);
thread.start();
try {
Thread.sleep(5000); // Main thread sleeps for 5 seconds
} catch (InterruptedException e) {
e.printStackTrace();
}
example.stop(); // Stop the running thread
}
}
In this example, the flag
variable is declared as volatile
, ensuring that changes to its value are visible across all threads. The stop()
method sets flag
to false
, causing the running thread to exit the loop and stop.
When to Use Volatile:
- Use
volatile
when you have simple flags or status variables that are accessed by multiple threads. - Do not use
volatile
for complex operations or variables that require atomicity, asvolatile
does not provide atomicity guarantees. For such cases, use synchronization or atomic variables likeAtomicInteger
.
Explain the Concept of the Java Memory Model (JMM).
The Java Memory Model (JMM) defines how threads interact through memory and what behaviors are allowed in a multithreaded environment. It specifies how variables are read from and written to memory, ensuring consistency and visibility of shared data across threads.
Key Concepts of JMM:
- Visibility: Changes made by one thread to shared variables are visible to other threads. This is achieved through synchronization constructs like
synchronized
,volatile
, and explicit locks. - Atomicity: Operations on shared variables are performed atomically, preventing partial updates.
- Ordering: The order in which operations are executed can affect the behavior of the program. JMM ensures proper ordering of operations through synchronization constructs.
- Happens-Before Relationship: The JMM defines a set of rules that establish a happens-before relationship between operations. If one action happens-before another, the first is visible and ordered before the second.
Example Code:
class MemoryModelExample {
private int counter = 0;
public synchronized void increment() {
counter++;
}
public synchronized int getCounter() {
return counter;
}
public static void main(String[] args) {
MemoryModelExample example = new MemoryModelExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final counter value: " + example.getCounter());
}
}
In this example, the synchronized
keyword ensures visibility, atomicity, and proper ordering of operations on the counter
variable, adhering to the rules of the Java Memory Model.
What is the ThreadLocal Class in Java, and When Would You Use It?
The ThreadLocal
class in Java provides thread
-local variables. Each thread accessing such a variable has its own, independently initialized copy of the variable. ThreadLocal
is useful when you want to avoid sharing variables between threads and ensure that each thread has its own instance of a variable.
Example Code:
class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public void increment() {
threadLocal.set(threadLocal.get() + 1);
}
public int getValue() {
return threadLocal.get();
}
public static void main(String[] args) {
ThreadLocalExample example = new ThreadLocalExample();
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
example.increment();
System.out.println(Thread.currentThread().getName() + ": " + example.getValue());
}
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
thread1.start();
thread2.start();
}
}
In this example, each thread has its own instance of the threadLocal
variable, ensuring that increments made by one thread do not affect the other thread.
When to Use ThreadLocal:
- Use
ThreadLocal
when you need a variable that is specific to each thread and should not be shared between threads. - It is useful for maintaining state or context information that is thread-specific, such as user sessions or transaction contexts.
What are the Key Differences Between Synchronized and ReentrantLock in Java?
Both synchronized
and ReentrantLock
are used for synchronization in Java, but they have key differences:
- Reentrancy:
- Both
synchronized
andReentrantLock
are reentrant, meaning a thread can acquire the same lock multiple times.
- Lock Acquisition:
synchronized
: Implicit lock acquisition and release.ReentrantLock
: Explicit lock acquisition and release with methodslock()
andunlock()
.
- Fairness:
synchronized
: No fairness guarantee.ReentrantLock
: Option to create a fair lock using theReentrantLock(boolean fair)
constructor.
- Condition Variables:
synchronized
: Uses intrinsic condition variables withwait()
,notify()
, andnotifyAll()
.ReentrantLock
: Uses explicitCondition
objects with methodsawait()
,signal()
, andsignalAll()
.
- Interruptible Lock Acquisition:
synchronized
: Not interruptible.ReentrantLock
: Supports interruptible lock acquisition withlockInterruptibly()
.
- Timeout:
synchronized
: No support for timeout.ReentrantLock
: Supports timeout withtryLock(long timeout, TimeUnit unit)
.
Example Code:
import java.util.concurrent.locks.ReentrantLock;
class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
In this example, ReentrantLock
is used for synchronization, providing more control over lock acquisition and release compared to the synchronized
keyword.
What is a CountDownLatch in Java?
A CountDownLatch
in Java is a synchronization aid that allows one or more threads to wait until a set of operations being performed by other threads completes. It has a counter that is initialized with a given count. Threads wait on the latch by calling await()
, and the counter is decremented by calls to countDown()
. When the counter reaches zero, all waiting threads are released.
Example Code:
import java.util.concurrent.CountDownLatch;
class CountDownLatchExample {
private final CountDownLatch latch = new CountDownLatch(3);
public void performTask() {
try {
System.out.println(Thread.currentThread().getName() + " is waiting...");
latch.await();
System.out.println(Thread.currentThread().getName() + " is proceeding...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void completeTask() {
System.out.println(Thread.currentThread().getName() + " has completed a task.");
latch.countDown();
}
public static void main(String[] args) {
CountDownLatchExample example = new CountDownLatchExample();
Runnable task = () -> {
example.performTask();
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
Thread thread3 = new Thread(task, "Thread 3");
thread1.start();
thread2.start();
thread3.start();
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
example.completeTask();
example.completeTask();
example.completeTask();
}
}
In this example, three threads wait on the latch until the count reaches zero, allowing them to proceed.
Explain the Usage of CyclicBarrier in Java.
A CyclicBarrier
in Java is a synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. The barrier is cyclic because it can be reused after the waiting threads are released. It is useful for scenarios where you want threads to synchronize at a certain point and then proceed together.
Example Code:
import java.util.concurrent.CyclicBarrier;
class CyclicBarrierExample {
private final CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All parties have arrived at the barrier. Proceeding...");
});
public void performTask() {
System.out.println(Thread.currentThread().getName() + " is waiting at the barrier...");
try {
barrier.await();
System.out.println(Thread.currentThread().getName() + " has crossed the barrier.");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CyclicBarrierExample example = new CyclicBarrierExample();
Runnable task = () -> {
example.performTask();
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
Thread thread3 = new Thread(task, "Thread 3");
thread1.start();
thread2.start();
thread3.start();
}
}
In this example, three threads wait at the barrier. When all threads reach the barrier, they proceed together, and the barrier is reset for reuse.
What is a Semaphore in Java, and How Does It Work?
A Semaphore
in Java is a synchronization aid that controls access to a shared resource through a set of permits. Threads acquire permits before accessing the resource and release them after use. If no permits are available, the thread blocks until a permit is released. Semaphores can be used to implement resource pools, limiting the number of concurrent accesses to a resource.
Example Code:
import java.util.concurrent.Semaphore;
class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(2); // 2 permits
public void accessResource() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " has acquired a permit.");
Thread.sleep(2000); // Simulate resource access
System.out.println(Thread.currentThread().getName() + " is releasing the permit.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}
public static void main(String[] args) {
SemaphoreExample example = new SemaphoreExample();
Runnable task = () -> {
example.accessResource();
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
Thread thread3 = new Thread(task, "Thread 3");
thread1.start();
thread2.start();
thread3.start();
}
}
In this example, only two threads can acquire permits and access the resource simultaneously. The third thread waits until a permit is released.
Explain the Executor Framework in Java.
The Executor framework in Java provides a high-level API for managing and controlling a pool of threads. It decouples task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. The framework consists of several key components:
- Executor Interface: The root interface for executing tasks.
Executor executor = Executors.newFixedThreadPool(10);
- ExecutorService Interface: Extends
Executor
and provides methods for managing lifecycle and asynchronous task execution.
ExecutorService executorService = Executors.newFixedThreadPool(10);
- ScheduledExecutorService Interface: Extends
ExecutorService
and supports scheduling tasks with delays or periodic execution.
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
Example Code:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class ExecutorExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " is executing the task.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has completed the task.");
};
executorService.submit(task);
executorService.submit(task);
executorService.submit(task);
executorService.shutdown();
}
}
In this example, a fixed thread pool is created with three threads, and three tasks are submitted for execution. The ExecutorService
manages the threads and task execution.
What are the Differences Between Executor, ExecutorService, and ScheduledExecutorService?
- Executor:
- The base interface for executing tasks.
- Provides a single
execute(Runnable command)
method. - Does not provide methods for managing the lifecycle or task completion.
Executor executor = Executors.newFixedThreadPool(10);
executor.execute(new RunnableTask());
- ExecutorService:
- Extends
Executor
and provides additional methods for managing the lifecycle and task execution. - Provides methods like
submit()
,invokeAll()
,invokeAny()
,shutdown()
,shutdownNow()
, etc.
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<?> future = executorService.submit(new CallableTask());
executorService.shutdown();
- ScheduledExecutorService:
- Extends
ExecutorService
and supports scheduling tasks with delays or periodic execution. - Provides methods like
schedule()
,scheduleAtFixedRate()
,scheduleWithFixedDelay()
.
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.schedule(new RunnableTask(), 5, TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
How Do You Create a Thread Pool in Java?
A thread pool can be created using the Executors
factory methods provided by the java.util.concurrent
package. Commonly used thread pool types include:
- FixedThreadPool: A thread pool with a fixed number of threads.
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
- CachedThreadPool: A thread pool that creates new threads as needed but reuses previously constructed threads when available.
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
- ScheduledThreadPool: A thread pool that can schedule commands to run after a given delay or periodically.
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
- SingleThreadExecutor: A thread pool with a single worker thread.
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
Example Code:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
Runnable task = () -> {
System.out.println(Thread.currentThread().getName() + " is executing the task.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has completed the task.");
};
fixedThreadPool.submit(task);
fixedThreadPool.submit(task);
fixedThreadPool.submit(task);
fixedThreadPool.shutdown();
}
}
In this example, a fixed thread pool is created with three threads, and three tasks are submitted for execution.
What is the ForkJoinPool in Java?
The ForkJoinPool
in Java is a specialized implementation of the ExecutorService
interface designed for parallel processing of tasks. It uses the fork/join framework to split tasks into smaller subtasks and then combine their results. The ForkJoinPool
is optimized for tasks that can be broken into smaller, independent subtasks, which can be processed in parallel.
Example Code:
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
class ForkJoinExample extends RecursiveTask<Long> {
private static final int THRESHOLD = 10_000;
private long start;
private long end;
public ForkJoinExample(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long middle = (start + end) / 2;
ForkJoinExample leftTask = new ForkJoinExample(start, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, end);
leftTask.fork();
long rightResult = rightTask.compute();
long leftResult = leftTask.join();
return leftResult + rightResult;
}
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinExample task = new ForkJoinExample(0, 1_000_000);
long result = forkJoinPool.invoke(task);
System.out.println("Sum: " + result);
}
}
In this example, the ForkJoinPool
is used to compute the sum of numbers from 0 to 1,000,000 by dividing the task into smaller subtasks and processing them in parallel.
Explain the Concept of a Callable and Future in Java.
A Callable
in Java is similar to a Runnable
but can return a result and throw a checked exception. The Future
represents the result of an asynchronous computation and provides methods to check if the computation is complete, wait for its completion, and retrieve the result.
Example Code:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class CallableExample implements Callable<Integer> {
private int number;
public CallableExample(int number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= number; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
CallableExample task = new CallableExample(10);
Future<Integer> future = executorService.submit(task);
try {
System.out.println("Sum: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
In this example, a Callable
task computes the sum of numbers from 1 to 10, and a Future
is used to retrieve the result asynchronously.
What is a CompletableFuture in Java, and How Does It Differ from a Future?
A CompletableFuture
in Java is an extension of the Future
interface that provides a more flexible and powerful way to handle asynchronous tasks. It supports a wide range of methods for combining, composing, and handling results of asynchronous computations.
Key Differences Between Future
and CompletableFuture
:
- Completion:
CompletableFuture
can be completed explicitly using thecomplete()
method, whereasFuture
can only be completed by the executing task. - Chaining:
CompletableFuture
supports method chaining and combining multiple futures using methods likethenApply()
,thenCompose()
,thenCombine()
, etc. - Asynchronous Execution:
CompletableFuture
provides methods for asynchronous execution, such assupplyAsync()
,runAsync()
, etc. - Exception Handling:
CompletableFuture
provides methods for handling exceptions in the computation pipeline, such asexceptionally()
,handle()
,whenComplete()
, etc.
Example Code:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
return 10;
}).thenApply(number -> {
return number * 2;
});
try {
System.out.println("Result: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
In this example, a CompletableFuture
is used to perform an asynchronous computation, multiply the result by 2, and retrieve the final result.
What are the Different States of a Thread in Java?
A thread in Java can be in one of the
following states:
- New: The thread is created but not yet started.
- Runnable: The thread is ready to run and is waiting for CPU time.
- Blocked: The thread is blocked waiting for a monitor lock.
- Waiting: The thread is waiting indefinitely for another thread to perform a particular action.
- Timed Waiting: The thread is waiting for another thread to perform a particular action for a specified period.
- Terminated: The thread has finished its execution.
Example Code:
class ThreadStateExample extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000); // Timed Waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread is running.");
}
public static void main(String[] args) {
ThreadStateExample thread = new ThreadStateExample();
System.out.println("Thread state: " + thread.getState()); // New
thread.start();
System.out.println("Thread state: " + thread.getState()); // Runnable
try {
Thread.sleep(500); // Main thread sleeps to let the child thread enter Timed Waiting
System.out.println("Thread state: " + thread.getState()); // Timed Waiting
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread state: " + thread.getState()); // Terminated
}
}
In this example, the thread transitions through different states, and the state is printed at various points.
How Do You Handle Exceptions in a Multi-Threaded Environment?
Handling exceptions in a multi-threaded environment requires careful consideration to ensure that exceptions are properly caught and handled without disrupting the entire application. Some common strategies include:
- Try-Catch Blocks: Use try-catch blocks within the
run()
method orcall()
method to catch exceptions.
class ExceptionHandlingExample extends Thread {
@Override
public void run() {
try {
throw new Exception("An error occurred.");
} catch (Exception e) {
System.out.println("Caught exception: " + e.getMessage());
}
}
public static void main(String[] args) {
ExceptionHandlingExample thread = new ExceptionHandlingExample();
thread.start();
}
}
- UncaughtExceptionHandler: Use an
UncaughtExceptionHandler
to handle uncaught exceptions.
class ExceptionHandlingExample extends Thread {
@Override
public void run() {
throw new RuntimeException("An error occurred.");
}
public static void main(String[] args) {
ExceptionHandlingExample thread = new ExceptionHandlingExample();
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Caught exception: " + e.getMessage());
});
thread.start();
}
}
- Future and Callable: Use
Future
andCallable
to handle exceptions in tasks submitted to anExecutorService
.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class ExceptionHandlingExample implements Callable<Integer> {
@Override
public Integer call() throws Exception {
throw new Exception("An error occurred.");
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(new ExceptionHandlingExample());
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("Caught exception: " + e.getMessage());
}
executorService.shutdown();
}
}
What is the ConcurrentHashMap Class in Java, and How Does It Differ from HashMap?
The ConcurrentHashMap
class in Java is a thread-safe implementation of the Map
interface that allows concurrent access to its elements without the need for explicit synchronization. It is designed for high concurrency and performance in multithreaded environments.
Key Differences Between ConcurrentHashMap
and HashMap
:
- Thread Safety:
HashMap
: Not thread-safe. Concurrent access can lead to data inconsistency and race conditions.ConcurrentHashMap
: Thread-safe. Allows concurrent access and modifications.
- Performance:
HashMap
: Better performance in single-threaded environments.ConcurrentHashMap
: Better performance in multithreaded environments due to internal partitioning.
- Null Values:
HashMap
: Allows null keys and values.ConcurrentHashMap
: Does not allow null keys and values.
- Iteration:
HashMap
: Iteration is not thread-safe and can lead toConcurrentModificationException
.ConcurrentHashMap
: Iteration is thread-safe and provides a snapshot of the elements at the time of iteration.
Example Code:
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
class ConcurrentHashMapExample {
public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>();
// Adding elements
map.put("1", "One");
map.put("2", "Two");
// Accessing elements
System.out.println("Value for key 1: " + map.get("1"));
// Iterating elements
map.forEach((key, value) -> {
System.out.println(key + ": " + value);
});
}
}
In this example, a ConcurrentHashMap
is used to store and access elements in a thread-safe manner.
Explain the Concept of Immutability and How It Relates to Concurrency.
Immutability is a concept where an object’s state cannot be modified after it is created. Immutable objects are inherently thread-safe because their state cannot change, eliminating the need for synchronization when accessing them from multiple threads.
Benefits of Immutability in Concurrency:
- Thread Safety: Immutable objects are inherently thread-safe, as their state cannot be altered after creation.
- No Synchronization: Since the state cannot change, there is no need for synchronization, reducing overhead and complexity.
- Safe Sharing: Immutable objects can be freely shared between threads without concerns about data inconsistency or race conditions.
Example Code:
final class ImmutableExample {
private final int value;
public ImmutableExample(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static void main(String[] args) {
ImmutableExample example = new ImmutableExample(42);
System.out.println("Value: " + example.getValue());
}
}
In this example, the ImmutableExample
class is immutable, as its state (the value
field) cannot be changed after it is initialized.
What is the AtomicInteger Class, and How Does It Help in Concurrent Programming?
The AtomicInteger
class in Java provides atomic operations on integer values, ensuring thread-safe access and modification without the need for explicit synchronization. It is part of the java.util.concurrent.atomic
package and uses low-level atomic operations provided by the JVM.
Key Features of AtomicInteger
:
- Atomic Operations: Provides atomic methods like
get()
,set()
,incrementAndGet()
,decrementAndGet()
,addAndGet()
, etc. - CAS (Compare-And-Set): Uses CAS operations to ensure atomicity and avoid race conditions.
- Thread Safety: Ensures thread-safe access and modification of the integer value without explicit synchronization.
Example Code:
import java.util.concurrent.atomic.AtomicInteger;
class AtomicIntegerExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
AtomicIntegerExample example = new AtomicIntegerExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
In this example, AtomicInteger
is used to perform atomic increments on the count
variable, ensuring thread-safe access and modification.
What is the Lock Interface in Java, and How Does It Work?
The Lock
interface in Java, part of the java.util.concurrent.locks
package, provides a more flexible and sophisticated way to control access to shared resources compared to the synchronized
keyword. It defines methods for acquiring, releasing, and checking the status of locks.
Key Methods of Lock
Interface:
- lock(): Acquires the lock. If the lock is not available, the current thread is blocked until the lock is acquired.
- lockInterruptibly(): Acquires the lock unless the current thread is interrupted.
- tryLock(): Tries to acquire the lock without blocking. Returns
true
if the lock was acquired,false
otherwise. - tryLock(long time, TimeUnit unit): Tries to acquire the lock within the given waiting time. Returns
true
if the lock was acquired,false
otherwise. - unlock(): Releases the lock.
Example Code:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class LockExample {
private final Lock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) {
LockExample example = new LockExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
In this example, ReentrantLock
is used to perform thread-safe increments on the count
variable, ensuring controlled access to the shared resource.
Explain the ReadWriteLock Interface and Its Implementation ReentrantReadWriteLock.
The ReadWriteLock
interface in Java, part of the java.util.concurrent.locks
package, defines a pair of locks: one for read-only operations and one for write operations. It allows multiple threads to read concurrently but only one thread to write at a time, improving concurrency in read-heavy scenarios.
Key Methods of ReadWriteLock
Interface:
- readLock(): Returns the lock used for read operations.
- writeLock(): Returns the lock used for write operations.
ReentrantReadWriteLock
is a concrete implementation of the ReadWriteLock
interface that supports reentrant locking for both read and write operations.
Example Code:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteLockExample {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private int value = 0;
public void write(int newValue) {
readWriteLock.writeLock().lock();
try {
value = newValue;
System.out.println("Value written: " + value);
} finally {
readWriteLock.writeLock().unlock();
}
}
public void read() {
readWriteLock.readLock().lock();
try {
System.out.println("Value read: " + value);
} finally {
readWriteLock.readLock().unlock();
}
}
public static void main(String[] args) {
ReadWriteLockExample example = new ReadWriteLockExample();
Runnable writeTask = () -> {
example.write(42);
};
Runnable readTask = () -> {
example.read();
};
Thread writer = new Thread(writeTask);
Thread reader1 = new Thread(readTask);
Thread reader2 = new Thread(readTask);
writer.start();
reader1.start();
reader2.start();
try {
writer.join();
reader1.join();
reader2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, ReentrantReadWriteLock
is used to allow concurrent read operations while ensuring exclusive access for write operations, improving concurrency and performance.
What is the Phaser Class in Java, and How Does It Work?
The Phaser
class in Java, part of the java.util.concurrent
package, is a flexible synchronization barrier that allows multiple threads to wait for each other at a common barrier point, similar to CyclicBarrier
but with more dynamic and flexible phase control. It can support a variable number of threads arriving at different phases.
Key Methods of Phaser
:
- arrive(): Arrives at the phaser and returns the arrival phase number.
- arriveAndAwaitAdvance(): Arrives at the phaser and waits for others to arrive.
- arriveAndDeregister(): Arrives at the phaser and deregisters from it.
- awaitAdvance(int phase): Waits for the phase to advance from the given phase number.
- bulkRegister(int parties): Registers the given number of new parties.
Example Code:
import java.util.concurrent.Phaser;
class PhaserExample {
private final Phaser phaser = new Phaser(1); // Main thread is the first registered party
public void task(int phaseToWaitFor) {
phaser.register();
try {
System.out.println(Thread.currentThread().getName() + " is waiting for phase " + phaseToWaitFor);
phaser.awaitAdvance(phaseToWaitFor);
System.out.println(Thread.currentThread().getName() + " is proceeding.");
} finally {
phaser.arriveAndDeregister();
}
}
public static void main(String[] args) {
PhaserExample example = new PhaserExample();
Runnable task = () -> {
example.task(0);
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
thread1.start();
thread2.start();
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is advancing the phase.");
example.phaser.arriveAndAwaitAdvance(); // Advance to the next phase
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, the Phaser
class is used to synchronize multiple threads at a common barrier point, allowing them to wait for each other and proceed together.
Explain the Exchanger Class in Java.
The Exchanger
class in Java, part of the java.util.concurrent
package, provides a synchronization point where two threads can exchange objects. Each thread presents an object at the exchange point, and then receives the object presented by the other thread.
Example Code:
import java.util.concurrent.Exchanger;
class ExchangerExample {
private final Exchanger<String> exchanger = new Exchanger<>();
public void task1() {
try {
String message = "Message from task1";
System.out.println("Task1 is exchanging: " + message);
String response = exchanger.exchange(message);
System.out.println("Task1 received: " + response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void task2() {
try {
String message = "Message from task2";
System.out.println("Task2 is exchanging: " + message);
String response = exchanger.exchange(message);
System.out.println("Task2 received: " + response);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExchangerExample example = new ExchangerExample();
Thread thread1 = new Thread(example::task1, "Thread 1");
Thread thread2 = new Thread(example::task2, "Thread 2");
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, Exchanger
is used to synchronize two threads at a common exchange point, allowing them to exchange messages.
What are the Key Differences Between ConcurrentLinkedQueue and LinkedBlockingQueue?
- Thread Safety:
- Both
ConcurrentLinkedQueue
andLinkedBlockingQueue
are thread-safe and designed for concurrent access.
- Blocking Behavior:
ConcurrentLinkedQueue
: Non-blocking, does not support blocking operations.LinkedBlockingQueue
: Blocking, supports blocking operations liketake()
andput()
.
- Capacity:
ConcurrentLinkedQueue
: Unbounded, does not have a fixed capacity.LinkedBlockingQueue
: Bounded or unbounded, can have a fixed capacity or be unbounded.
- Performance:
ConcurrentLinkedQueue
: Generally provides better performance for high-concurrency, non-blocking scenarios.LinkedBlockingQueue
: Provides better control over resource usage and supports blocking operations.
Example Code:
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
class QueueExample {
public static void main(String[] args) {
ConcurrentLinkedQueue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
LinkedBlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>(10);
// Adding elements to ConcurrentLinkedQueue
concurrentQueue.add("Element 1");
concurrentQueue.add("Element 2");
// Adding elements to LinkedBlockingQueue
try {
blockingQueue.put("Element 1");
blockingQueue.put("Element 2");
} catch (InterruptedException e) {
e.printStackTrace();
}
// Accessing elements from ConcurrentLinkedQueue
System.out.println("ConcurrentLinkedQueue: " + concurrentQueue.poll());
// Accessing elements from LinkedBlockingQueue
try {
System.out.println("LinkedBlockingQueue: " + blockingQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, ConcurrentLinkedQueue
is used for non-blocking operations, while LinkedBlockingQueue
is used for blocking operations.
What is the Difference Between CountDownLatch and CyclicBarrier?
- Purpose:
CountDownLatch
: Used for waiting for a set of operations to complete. The count cannot be reset.CyclicBarrier
: Used for synchronizing multiple threads at a common barrier point. The barrier can be reused.
- Resetting:
CountDownLatch
: Cannot be reset once the count reaches zero.CyclicBarrier
: Can be reset and reused.
- Parties:
CountDownLatch
: Any thread can callcountDown()
, and any thread can wait on the latch.CyclicBarrier
: Only the threads involved in the barrier can callawait()
.
Example Code for CountDownLatch:
import java.util.concurrent.CountDownLatch;
class CountDownLatchExample {
private final CountDownLatch latch = new CountDownLatch(3);
public void performTask() {
try {
System.out.println(Thread.currentThread().getName() + " is waiting...");
latch.await();
System.out.println(Thread.currentThread().getName() + " is proceeding...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void completeTask() {
System.out.println(Thread.currentThread().getName() + " has completed a task.");
latch.countDown();
}
public static void main(String[] args) {
CountDownLatchExample example = new CountDownLatchExample();
Runnable task = () -> {
example.performTask();
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
Thread thread3 = new Thread(task, "Thread 3");
thread1.start();
thread2.start();
thread3.start();
try {
Thread.sleep(1000); // Simulate some work
} catch (InterruptedException e) {
e.printStackTrace();
}
example.completeTask();
example.completeTask();
example.completeTask();
}
}
Example Code for CyclicBarrier:
import java.util.concurrent.CyclicBarrier;
class CyclicBarrierExample {
private final CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All parties have arrived at the barrier. Proceeding...");
});
public void performTask() {
System.out.println(Thread.currentThread().getName() + " is waiting at the barrier...");
try {
barrier.await();
System.out.println(Thread.currentThread().getName() + " has crossed the barrier.");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CyclicBarrierExample example = new CyclicBarrierExample();
Runnable task = () -> {
example.performTask();
};
Thread thread1 = new Thread(task, "Thread 1");
Thread thread2 = new Thread(task, "Thread 2");
Thread thread3 = new Thread(task, "Thread 3");
thread1.start();
thread2.start();
thread3.start();
}
}
Explain the Concept of Thread Priority in Java.
Thread priority in Java determines the relative priority of threads for CPU allocation. Threads with higher priority are more likely to be executed before threads with lower priority, although this is not guaranteed.
Key Points:
- Priority Range: Thread priority ranges from
Thread.MIN_PRIORITY
(1) toThread.MAX_PRIORITY
(10). The default priority isThread.NORM_PRIORITY
(5). - Setting Priority: Thread priority can be set using the
setPriority(int newPriority)
method. - Priority Inheritance: When a thread creates a new thread, the new thread inherits the priority of the creating thread.
- Scheduling: Thread scheduling is platform-dependent, and priorities may not be strictly followed by all JVM implementations.
Example Code:
class ThreadPriorityExample extends Thread {
public ThreadPriorityExample(String name) {
super(name);
}
@Override
public void run() {
System.out.println(getName() + " is running with priority " + getPriority());
}
public static void main(String[] args) {
ThreadPriorityExample thread1 = new ThreadPriorityExample("Thread 1");
ThreadPriorityExample thread2 = new ThreadPriorityExample("Thread 2");
ThreadPriorityExample thread3 = new ThreadPriorityExample("Thread 3");
thread1.setPriority(Thread.MIN_PRIORITY);
thread2.setPriority(Thread.NORM_PRIORITY);
thread3.setPriority(Thread.MAX_PRIORITY);
thread1.start();
thread2.start();
thread3.start();
}
}
In this example, three threads are created with different priorities, and their priorities are printed when they run.
How Do You Ensure That a Multi-Threaded Application is Thread-Safe?
Ensuring thread safety in a multi-threaded application involves several strategies to prevent data inconsistency, race conditions, and other concurrency issues:
- Synchronization: Use synchronized methods or blocks to control access to shared resources.
public synchronized void increment() {
count++;
}
- Locks: Use explicit locks (e.g.,
ReentrantLock
) to control access to shared resources.
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
- Atomic Variables: Use atomic variables (e.g.,
AtomicInteger
) for simple atomic operations.
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
- Immutability: Use immutable objects to ensure that their state cannot be modified after creation.
final class ImmutableExample {
private final int value;
public ImmutableExample(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
- Thread-Local Variables: Use
ThreadLocal
variables to ensure that each thread has its own instance of a variable.
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public void increment() {
threadLocal.set(threadLocal.get() + 1);
}
public int getValue() {
return threadLocal.get();
}
- Concurrent Collections: Use concurrent collections (e.g.,
ConcurrentHashMap
,CopyOnWriteArrayList
) that provide thread-safe operations.
private Map<String, String> map = new ConcurrentHashMap<>();
public void put(String key, String value) {
map.put(key, value);
}
public String get(String key) {
return map.get(key);
}
Example Code:
import java.util.concurrent.atomic.AtomicInteger;
class ThreadSafeExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
ThreadSafeExample example = new ThreadSafeExample();
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + example.getCount());
}
}
In this example, AtomicInteger
is used to ensure thread-safe increments on the count
variable.
What is the FutureTask Class in Java, and How Does It Work?
The FutureTask
class in Java, part of the java.util.concurrent
package, represents a cancellable asynchronous computation. It implements Runnable
and Future
, allowing it to be used as a task that can be executed by an ExecutorService
and provide a result.
Key Features of FutureTask
:
- Cancellable: The task can be cancelled using the
cancel()
method. - Asynchronous Result: The result of the computation can be retrieved using the
get()
method, which blocks until the computation is complete. - Runnable: The task can be submitted to an
ExecutorService
for execution.
Example Code:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
class FutureTaskExample {
public static void main(String[] args) {
Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(futureTask);
try {
System.out.println("Result: " + futureTask.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executorService.shutdown();
}
}
In this example, a FutureTask
is
created with a Callable
that returns a result after a delay. The task is submitted to an ExecutorService
for execution, and the result is retrieved using the get()
method.
Explain the Concept of java.util.concurrent Package in Java.
The java.util.concurrent
package in Java provides a comprehensive set of classes and interfaces for building concurrent applications. It includes thread-safe collections, synchronization primitives, and high-level utilities for managing and coordinating multiple threads.
Key Components of java.util.concurrent
Package:
- Concurrent Collections: Thread-safe collections like
ConcurrentHashMap
,CopyOnWriteArrayList
,ConcurrentLinkedQueue
, etc. - Locks: Locking mechanisms like
ReentrantLock
,ReadWriteLock
,StampedLock
, etc. - Synchronization Aids: Utilities like
CountDownLatch
,CyclicBarrier
,Semaphore
,Phaser
, etc. - Executors: High-level API for managing and controlling a pool of threads, including
Executor
,ExecutorService
,ScheduledExecutorService
,ForkJoinPool
, etc. - Atomic Variables: Classes for atomic operations like
AtomicInteger
,AtomicLong
,AtomicReference
, etc.
Example Code:
import java.util.concurrent.*;
class ConcurrentPackageExample {
public static void main(String[] args) {
// Concurrent Collection
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 42);
System.out.println("Value: " + map.get("key"));
// Lock
Lock lock = new ReentrantLock();
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}
// CountDownLatch
CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
System.out.println("Task executed.");
latch.countDown();
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All tasks completed.");
// Executor
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
System.out.println("Task 1 executed.");
});
executorService.submit(() -> {
System.out.println("Task 2 executed.");
});
executorService.shutdown();
}
}
In this example, various components of the java.util.concurrent
package are used to build a concurrent application.
What is the ForkJoinTask Class in Java?
The ForkJoinTask
class in Java is an abstract class representing a task that can be executed within a ForkJoinPool
. It provides a framework for parallel execution of tasks that can be broken down into smaller subtasks. The ForkJoinTask
class is the base class for RecursiveTask
and RecursiveAction
.
Example Code:
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
class SumTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 10_000;
private long start;
private long end;
public SumTask(long start, long end) {
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
long middle = (start + end) / 2;
SumTask leftTask = new SumTask(start, middle);
SumTask rightTask = new SumTask(middle + 1, end);
leftTask.fork();
long rightResult = rightTask.compute();
long leftResult = leftTask.join();
return leftResult + rightResult;
}
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
SumTask task = new SumTask(0, 1_000_000);
long result = forkJoinPool.invoke(task);
System.out.println("Sum: " + result);
}
}
In this example, SumTask
extends RecursiveTask
and implements the compute()
method to divide the task into smaller subtasks and combine their results.
How Does the ForkJoinPool Improve Performance in Java?
The ForkJoinPool
improves performance in Java by using a work-stealing algorithm, which efficiently distributes tasks among worker threads. It allows worker threads that have completed their tasks to “steal” tasks from other busy worker threads, ensuring better utilization of CPU resources and faster completion of tasks.
Key Features of ForkJoinPool
:
- Work-Stealing: Idle worker threads steal tasks from busy worker threads, balancing the workload.
- Parallelism: Executes tasks in parallel by dividing them into smaller subtasks.
- Efficient Task Management: Manages task queues and worker threads efficiently to minimize overhead.
Example Code:
import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;
class FibonacciTask extends RecursiveTask<Integer> {
private int n;
public FibonacciTask(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
FibonacciTask task1 = new FibonacciTask(n - 1);
task1.fork();
FibonacciTask task2 = new FibonacciTask(n - 2);
return task2.compute() + task1.join();
}
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
FibonacciTask task = new FibonacciTask(10);
int result = forkJoinPool.invoke(task);
System.out.println("Fibonacci(10): " + result);
}
}
In this example, the ForkJoinPool
improves performance by executing the Fibonacci computation in parallel, leveraging the work-stealing algorithm to balance the workload among worker threads.
What is a BlockingQueue in Java, and How Does It Work?
A BlockingQueue
in Java, part of the java.util.concurrent
package, is a thread-safe 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 storing an element. It is useful for implementing producer-consumer patterns.
Key Methods of BlockingQueue
:
- put(E e): Inserts the specified element into the queue, waiting if necessary for space to become available.
- take(): Retrieves and removes the head of the queue, waiting if necessary until an element becomes available.
- offer(E e, long timeout, TimeUnit unit): Inserts the specified element into the queue, waiting up to the specified time if necessary for space to become available.
- poll(long timeout, TimeUnit unit): Retrieves and removes the head of the queue, waiting up to the specified time if necessary for an element to become available.
Example Code:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class Producer implements Runnable {
private BlockingQueue<String> queue;
public Producer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
queue.put("Item 1");
System.out.println("Produced: Item 1");
Thread.sleep(1000);
queue.put("Item 2");
System.out.println("Produced: Item 2");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable {
private BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
String item = queue.take();
System.out.println("Consumed: " + item);
item = queue.take();
System.out.println("Consumed: " + item);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class BlockingQueueExample {
public static void main(String[] args) {
BlockingQueue<String> queue = new LinkedBlockingQueue<>(2);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
Thread producerThread = new Thread(producer);
Thread consumerThread = new Thread(consumer);
producerThread.start();
consumerThread.start();
try {
producerThread.join();
consumerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
In this example, BlockingQueue
is used to implement a producer-consumer pattern, where the producer thread produces items and the consumer thread consumes them, waiting as necessary for the queue to become non-empty or have available space.