Q41.

What is the difference between fail-fast and fail-safe iterator?

Q42.

Explain autoboxing and unboxing.

Autoboxing and unboxing are features in Java that allow seamless conversion between primitive types and their corresponding wrapper classes. Introduced in Java 5, these features simplify code by automatically handling the conversions, reducing the need for explicit casting.

What are Wrapper Classes?

In Java, primitive data types (like int, char, boolean, double) are not objects. To enable primitive values to be treated as objects, Java provides wrapper classes (e.g., Integer, Character, Boolean, Double). These classes encapsulate primitive values within an object, allowing them to be used in contexts where objects are required, such as collections or generics.

Autoboxing

Autoboxing is the automatic conversion performed by the Java compiler from a primitive type to its corresponding wrapper class object. For example, converting an int to an Integer object. This process happens implicitly, meaning you don't need to explicitly create a new Integer(value).

  • Automatic conversion from primitive to wrapper object.
  • Simplifies code, making it less verbose.
  • Introduced in Java 5.
java
int primitiveInt = 100;
Integer wrapperInt = primitiveInt; // Autoboxing: int to Integer

// Example with method arguments
void printInteger(Integer i) {
    System.out.println(i);
}
printInteger(200); // Autoboxing: int 200 is converted to Integer object

When does Autoboxing Occur?

  • Assigning a primitive value to a wrapper class reference.
  • Passing a primitive value to a method that expects an object of the corresponding wrapper class.
  • Storing primitive values in collections (which store objects).

Unboxing

Unboxing is the automatic conversion performed by the Java compiler from a wrapper class object to its corresponding primitive type. For example, converting an Integer object to an int primitive. This process also happens implicitly.

  • Automatic conversion from wrapper object to primitive type.
  • Complements autoboxing.
  • Also introduced in Java 5.
java
Integer wrapperInt = Integer.valueOf(50);
int primitiveInt = wrapperInt; // Unboxing: Integer to int

// Example with arithmetic operations
Integer a = 5; // Autoboxing
int b = a + 2; // Unboxing of 'a' to perform addition
System.out.println(b); // Output: 7

When does Unboxing Occur?

  • Assigning a wrapper object to a primitive type variable.
  • Passing a wrapper object to a method that expects a primitive type.
  • When a wrapper object is used in an arithmetic or relational operation with a primitive type, it is unboxed for the operation.

Advantages and Disadvantages

AspectDescription
Advantage (Code Simplicity)Reduces boilerplate code by removing the need for explicit conversions, making the code cleaner and more readable.
Advantage (Collections)Facilitates using primitive types directly with Java Collections Framework (e.g., `ArrayList<Integer>`), which only stores objects.
Disadvantage (Performance)Can introduce overhead due to the creation of unnecessary objects (autoboxing) or repeated conversions, potentially impacting performance in performance-critical applications.
Disadvantage (NullPointerException)Unboxing a `null` wrapper object will result in a `NullPointerException` at runtime. For example, `Integer myInt = null; int val = myInt;` will throw an error.

Performance Considerations

While convenient, autoboxing and unboxing should be used judiciously, especially in loops or performance-sensitive code. Excessive autoboxing can lead to the creation of many temporary objects, increasing memory consumption and potentially triggering more frequent garbage collection, which can degrade application performance. It's often better to use primitives directly when object semantics are not strictly required, or to use explicit conversions when performance is paramount.

Q43.

What is the difference between interface and functional interface?

Q44.

What is a marker interface?

Q45.

What is daemon thread?

Q46.

What is ExecutorService?

ExecutorService is a high-level API in Java's `java.util.concurrent` package used for managing threads. It provides a framework for asynchronously executing tasks, managing a pool of threads, and abstracting the complexities of thread creation and lifecycle management from the application developer. It's a key component for building concurrent applications.

What is ExecutorService?

At its core, ExecutorService separates task submission from task execution. Instead of creating a new thread for every task, you submit tasks to an ExecutorService instance, which then uses an internal pool of worker threads to execute them. This approach significantly reduces the overhead associated with thread creation and termination, leading to more efficient resource utilization and improved application performance.

Key Benefits

  • Thread Reuse: Avoids the overhead of creating new threads for each task.
  • Reduced Resource Consumption: Limits the number of concurrent threads, preventing system overload.
  • Improved Responsiveness: Allows the main application thread to continue execution without waiting for tasks to complete.
  • Task Management: Provides mechanisms to submit tasks, retrieve results (via Future), and manage the shutdown process.
  • Separation of Concerns: Decouples task definition from thread management.

Core Interfaces and Classes

The java.util.concurrent package provides several important components related to ExecutorService:

  • Executor: The base interface for executing submitted Runnable tasks. ExecutorService extends this interface.
  • ExecutorService: An extension of Executor that adds features for managing the lifecycle of the executor, including methods to shut down the thread pool and to produce Future objects for tracking the progress of submitted tasks.
  • Executors: A utility class that provides factory methods for creating common ExecutorService configurations.
  • Callable<V>: Represents a task that returns a result and might throw an exception. Can be submitted to an ExecutorService.
  • Runnable: Represents a task that performs an action but does not return a result. Can be submitted to an ExecutorService.

Creating an ExecutorService

The Executors utility class is commonly used to create different types of ExecutorService instances:

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceCreation {
    public static void main(String[] args) {
        // Creates a thread pool that reuses a fixed number of threads.
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        System.out.println("Fixed Thread Pool created.");

        // Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available.
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        System.out.println("Cached Thread Pool created.");

        // Creates an Executor that uses a single worker thread operating off an unbounded queue.
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        System.out.println("Single Thread Executor created.");

        // Don't forget to shut down the executors when done!
        fixedThreadPool.shutdown();
        cachedThreadPool.shutdown();
        singleThreadExecutor.shutdown();
    }
}

Submitting Tasks

Tasks can be submitted using execute() for Runnable tasks (no return value) or submit() for Runnable or Callable tasks (returns a Future).

java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.Callable;

public class TaskSubmission {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // Submitting a Runnable task
        executor.execute(() -> {
            System.out.println("Runnable task executing.");
        });

        // Submitting a Callable task
        Callable<String> callableTask = () -> {
            Thread.sleep(1000);
            return "Callable task completed.";
        };
        Future<String> future = executor.submit(callableTask);

        System.out.println("Main thread continues...");

        // Getting the result from the Future (this blocks until the task is done)
        String result = future.get();
        System.out.println("Result from Callable: " + result);

        executor.shutdown();
    }
}

Shutting Down the ExecutorService

It is crucial to shut down an ExecutorService when it's no longer needed to release system resources. Otherwise, the program might not terminate, as the threads remain active.

  • shutdown(): Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. It does not wait for previously submitted tasks to complete execution.
  • shutdownNow(): Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. There is no guarantee that actively executing tasks will stop.
  • awaitTermination(): Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first.
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ShutdownExecutor {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        for (int i = 0; i < 5; i++) {
            int taskId = i;
            executor.execute(() -> {
                try {
                    System.out.println("Task " + taskId + " started.");
                    Thread.sleep(1000); // Simulate work
                    System.out.println("Task " + taskId + " finished.");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.out.println("Task " + taskId + " interrupted.");
                }
            });
        }

        executor.shutdown(); // Initiate orderly shutdown

        try {
            // Wait for all tasks to complete for a maximum of 5 seconds
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                System.err.println("Executor did not terminate in the specified time.");
                executor.shutdownNow(); // Force shutdown if tasks are still running
                if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
                    System.err.println("Executor did not terminate.");
                }
            }
        } catch (InterruptedException ie) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
        System.out.println("ExecutorService shut down.");
    }
}

Common ExecutorService Types

Factory MethodDescriptionUse Case
`newFixedThreadPool(int nThreads)`Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.Good for CPU-bound tasks, where you want to limit the number of active threads to the number of available cores.
`newCachedThreadPool()`Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available. If no threads are available, it creates a new one. Unused threads terminate after 60 seconds of inactivity.Suitable for applications with many short-lived asynchronous tasks, where the total number of tasks is unknown or very large. Can be resource-intensive if tasks keep arriving without existing threads becoming available.
`newSingleThreadExecutor()`Creates an Executor that uses a single worker thread operating off an unbounded queue. Tasks are guaranteed to execute sequentially.Useful when you need to ensure tasks are executed in a specific order, or when you want to avoid concurrency issues for a set of tasks.
`newScheduledThreadPool(int corePoolSize)`Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.Ideal for tasks that need to be run at specific times or intervals, like recurring clean-up operations or timed events.
Q47.

Explain ConcurrentHashMap.

ConcurrentHashMap is a thread-safe, high-performance implementation of the Map interface in Java's concurrency framework. It provides significantly better concurrency than its predecessors like Hashtable or Collections.synchronizedMap, making it an ideal choice for applications requiring high throughput and concurrent access to map data without explicit external synchronization.

What is ConcurrentHashMap?

ConcurrentHashMap is designed to handle concurrent access from multiple threads efficiently and safely. It achieves thread safety and high concurrency by allowing multiple threads to read and write to different parts of the map simultaneously, without blocking the entire map for most operations. This contrasts sharply with Hashtable, which locks the entire map for nearly every operation, leading to poor performance under high contention.

Key Features and Benefits

  • High Concurrency: Multiple threads can operate on different parts of the map concurrently without conflicting.
  • Thread Safety: All operations are inherently thread-safe, eliminating the need for external synchronization.
  • Non-Blocking Reads: Read operations typically do not block concurrent write operations, leading to high read throughput.
  • Weakly Consistent Iterators: Iterators are 'weakly consistent', meaning they reflect the state of the map at the time of their creation and may not reflect subsequent modifications, but they will never throw a ConcurrentModificationException.
  • Performance: Offers superior performance compared to Hashtable and Collections.synchronizedMap in multi-threaded environments due to its fine-grained locking and non-blocking mechanisms.

How it Works Internally (Java 8 Onwards)

Prior to Java 8, ConcurrentHashMap used a segmented locking mechanism where the map was divided into several segments, each protected by a separate lock. This allowed concurrent modifications to different segments. Java 8 introduced a significant revision to its internal implementation to achieve even finer-grained locking and better performance.

In Java 8 and later, ConcurrentHashMap uses a combination of synchronized blocks on individual hash bins (array indices) and Compare-And-Swap (CAS) operations. Instead of segment locks, each Node (entry) in a hash table bucket can be locked independently when required, specifically during structural modifications like adding or removing elements. Read operations, for the most part, leverage volatile reads and do not require explicit locking, greatly contributing to high read concurrency.

Comparison with other Concurrent Maps

FeatureConcurrentHashMapHashtableCollections.synchronizedMap(HashMap)
ConcurrencyHigh (fine-grained locking/CAS)Low (full map lock)Low (full map lock)
Null Keys/ValuesNo/NoNo/NoYes/Yes
Iteration SafetyWeakly consistentFail-fastFail-fast
PerformanceExcellent in concurrent environmentsPoor in high contentionPoor in high contention
Introduced InJava 1.5Java 1.0Java 1.2

Common Use Cases

  • Caching: Ideal for implementing thread-safe caches where data is frequently accessed and potentially updated by multiple threads.
  • Storing Session Data: Managing user session information in web applications where concurrent access to sessions is common.
  • Implementing Thread-Safe Counters/Statistics: For accumulating counts or statistics from various threads in a performant manner.
  • Concurrent Data Structures: Serves as a fundamental building block for more complex concurrent data structures and algorithms.

Example Usage

Here's a simple Java example demonstrating basic operations with ConcurrentHashMap:

java
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> userScores = new ConcurrentHashMap<>();

        // Adding elements
        userScores.put("Alice", 100);
        userScores.put("Bob", 150);
        userScores.put("Charlie", 120);

        System.out.println("Initial scores: " + userScores);

        // Getting elements
        int aliceScore = userScores.get("Alice");
        System.out.println("Alice's score: " + aliceScore);

        // Updating an element (atomically using computeIfPresent)
        // Increment Bob's score by 50 if present
        userScores.computeIfPresent("Bob", (key, val) -> val + 50);
        System.out.println("Scores after Bob's update: " + userScores);

        // Removing an element
        userScores.remove("Charlie");
        System.out.println("Scores after Charlie's removal: " + userScores);

        // Iterating (weakly consistent)
        userScores.forEach((user, score) ->
            System.out.println("User: " + user + ", Score: " + score)
        );
    }
}

Conclusion

ConcurrentHashMap stands as an indispensable utility in Java's concurrency framework. Its sophisticated design for managing concurrent access to map data makes it the preferred choice for multi-threaded applications demanding both high performance and robust thread safety. It significantly outperforms older alternatives in high-contention scenarios, enabling developers to build scalable and efficient concurrent systems.

Q48.

What is the difference between wait() and sleep()?

Both wait() and sleep() methods in Java are used to pause the execution of a thread. However, they differ significantly in their mechanism, purpose, and interaction with locks and monitors.

Overview

In Java's multithreading environment, controlling thread execution is crucial for resource management and synchronization. While both wait() and sleep() temporarily halt a thread, understanding their distinct behaviors—especially concerning object monitors and thread states—is vital for writing correct and efficient concurrent applications.

Key Differences

Featurewait()sleep()
ClassObject classThread class
Monitor ReleaseReleases the monitor/lockDoes NOT release the monitor/lock
Usage ContextUsed for inter-thread communication (producer-consumer)Used for introducing a pause for a specific duration
Wake UpCan be woken up by notify() or notifyAll()Wakes up automatically after the specified time or by interrupt()
SynchronizationMust be called inside a synchronized block/methodCan be called anywhere
ArgumentTakes an optional timeout (long)Takes a mandatory time (long)
Static/Non-staticNon-staticStatic

wait() method

  • Belongs to the Object class.
  • Must be called from within a synchronized block or method, otherwise it throws an IllegalMonitorStateException.
  • When wait() is called, the current thread releases the lock it holds on the object and goes into a waiting state.
  • It can be woken up by another thread calling notify() or notifyAll() on the same object, or if the specified timeout expires.
  • Primarily used for inter-thread communication, often in producer-consumer scenarios.
java
public class WaitExample {
    private final Object monitor = new Object();
    private boolean messageReady = false;
    private String message = null;

    public void producer() {
        synchronized (monitor) {
            System.out.println("Producer: Producing message...");
            this.message = "Hello from producer!";
            this.messageReady = true;
            monitor.notify(); // Notify waiting consumer
            System.out.println("Producer: Message produced and notified.");
        }
    }

    public void consumer() {
        synchronized (monitor) {
            while (!messageReady) {
                try {
                    System.out.println("Consumer: Message not ready, waiting...");
                    monitor.wait(); // Release monitor and wait
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.out.println("Consumer: Interrupted while waiting.");
                }
            }
            System.out.println("Consumer: Received message: " + message);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WaitExample example = new WaitExample();
        Thread producerThread = new Thread(example::producer);
        Thread consumerThread = new Thread(example::consumer);

        consumerThread.start();
        Thread.sleep(100); // Give consumer a chance to start and wait
        producerThread.start();

        producerThread.join();
        consumerThread.join();
    }
}

sleep() method

  • Belongs to the Thread class and is a static method.
  • Can be called from any part of the code, not necessarily within a synchronized block.
  • When sleep() is called, the current thread temporarily stops execution for a specified duration but DOES NOT release any locks it holds.
  • After the specified time, the thread automatically moves back to the runnable state.
  • It's typically used to introduce a pause or delay in thread execution.
java
public class SleepExample {
    public static void main(String[] args) {
        System.out.println("Main Thread: Starting...");
        try {
            System.out.println("Main Thread: Going to sleep for 2 seconds.");
            Thread.sleep(2000); // Sleep for 2000 milliseconds (2 seconds)
            System.out.println("Main Thread: Woke up after 2 seconds.");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("Main Thread: Interrupted while sleeping.");
        }
        System.out.println("Main Thread: Exiting.");
    }
}

Conclusion

The fundamental distinction between wait() and sleep() lies in their lock handling. wait() is essential for proper inter-thread communication, releasing the monitor to allow other threads to acquire it. sleep() on the other hand, is a simple time-based pause that retains all acquired locks, making it suitable for delays rather than coordination.

Q49.

Explain Java memory model.

The Java Memory Model (JMM) defines how threads interact with memory and how changes made by one thread become visible to others. It specifies guarantees about when writes made by one thread are visible to reads made by another thread, ensuring consistent behavior across different hardware architectures and Java Virtual Machine (JVM) implementations. The JMM is crucial for understanding concurrency in Java, addressing issues of visibility, ordering, and atomicity.

Core Concepts of JMM

The JMM is based on the concepts of main memory and working memory (also known as local memory or cache) for each thread. When a thread accesses a variable, it makes a copy of it in its working memory. All operations (read, write) are performed on this working copy. The JMM specifies when changes made to a variable in one thread's working memory are flushed back to main memory and when another thread's working memory copy is invalidated or updated from main memory.

Happens-Before Relationship

The happens-before relationship is the cornerstone of the JMM. It is a partial ordering of all actions within a program that guarantees memory visibility. If action A happens-before action B, then the effects of A are visible to B, and A must precede B. Without a happens-before relationship, the JVM is free to reorder operations for performance, which can lead to unexpected results in concurrent programs.

  • Program Order Rule: Each action in a thread happens-before every subsequent action in that same thread.
  • Monitor Lock Rule: An unlock on a monitor happens-before every subsequent lock on that same monitor.
  • Volatile Variable Rule: A write to a volatile variable happens-before every subsequent read of that same volatile variable.
  • Thread Start Rule: A Thread.start() on a thread happens-before any action in the started thread.
  • Thread Termination Rule: All actions in a thread happen-before any other thread detects that thread has terminated (e.g., via Thread.join() or isAlive() returning false).
  • Interruption Rule: A call to Thread.interrupt() happens-before the interrupted thread detects the interrupt (by throwing InterruptedException or calling isInterrupted()).
  • Finalizer Rule: The constructor of an object happens-before the start of its finalizer.
  • Transitivity: If A happens-before B, and B happens-before C, then A happens-before C.

Key JMM Constructs

Java provides several language constructs that leverage the JMM to establish happens-before relationships and ensure proper memory synchronization.

Volatile Keyword

The volatile keyword ensures that reads and writes to a variable are directly from and to main memory, preventing a thread from caching the value locally. It guarantees visibility of changes across threads but does not provide atomicity for compound operations. The Volatile Variable Rule establishes a happens-before relationship.

java
class SharedData {
    volatile boolean flag = false;

    public void setFlag() {
        flag = true; // Write to volatile
    }

    public void checkFlag() {
        if (flag) { // Read from volatile
            System.out.println("Flag is true.");
        }
    }
}

Synchronized Keyword

The synchronized keyword provides mutual exclusion and memory visibility. When a thread enters a synchronized block or method, it acquires a monitor lock. Upon exiting, it releases the lock. The Monitor Lock Rule ensures that an unlock on a monitor happens-before any subsequent lock on that same monitor, guaranteeing that all changes made inside the synchronized block are visible to any other thread subsequently acquiring the same lock.

java
class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++; // Atomic and visible operation
    }

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

Final Fields

The JMM guarantees that once an object's constructor finishes, all its final fields are visible to other threads without explicit synchronization. This means that after a properly constructed object is published, all threads are guaranteed to see the correct values of its final fields immediately.

ConstructMemory VisibilityAtomicity
volatileGuaranteedNo (for compound ops)
synchronizedGuaranteedGuaranteed (for block)
final fieldsGuaranteed (after construction)N/A

Importance and Common Pitfalls

Understanding the JMM is vital for writing correct and performant concurrent applications. Misunderstanding it can lead to subtle bugs such as stale data reads, lost updates, or deadlock. Always ensure proper synchronization mechanisms are in place when multiple threads access shared mutable state.

  • Data Races: When multiple threads access the same shared mutable data without proper synchronization, and at least one access is a write.
  • Stale Data: A thread reads an outdated value of a variable because another thread's write is not yet visible.
  • Reordering: The JVM and hardware can reorder instructions for performance, which can break program logic if not constrained by happens-before rules.
Q50.

What are generics in Java?