Core Java Interview Essentials: Collections, Concurrency, JVM, and Common Frameworks

Redis Caching Strategies and Reliability

Cache Penetration

A common problem arises when querying for data that does not exist either in the cache or the database. Each such request bypasses the cache, generating unnecessary data base load.

Solution One: Null Object Caching If a query returns no data from the database, store a null or empty result in the cache with a short expiration. This blocks subsequent identical queries from hitting the database.

  • Advantage: Very simple to implement.
  • Disadvantage: Consumes extra memory and can lead to temporary data inconsistencies if the underlying data is later created.

Solution Two: Bloom Filter A Bloom filter is a space-efficient probabilistic data structure built on a bitmap. It tests whether an element is a member of a set. Before querying the database, the filter is checked. If it reports the element is absent, the request is rejected immediately. If it reports presence, the cache and database are queried normally.

  • Advantage: Low memory footprint, no redundant cache keys.
  • Disadvantage: Implementation is more complex and introduces a small, manageable false-positive rate.

The false positive rate is inversely proportional to the bitmap size; larger arrays reduce errors but increase memory usage.

Cache Avalanche

A cache avalanche occurs when a large number of cached keys expire simultaneously, or a Redis service crashes, causing a sudden flood of requests to the database.

Mitigation approaches include:

  1. Adding a random time-to-live (TTL) to each key to stagger expiration.
  2. Deploying Redis in a high-availability cluster mode (Sentinel or Cluster).
  3. Implementing a fallback or rate-limiting strategy at the gateway level using tools like Nginx or Spring Cloud Gateway.
  4. Introducing a multi-level caching architecture with local caches like Guava or Caffeine.

Data Consistency in Dual-Write Scenarios

When data is modified in the database, the corresponding cache must be updated to reflect the change.

A common16 approach is delayed double deletion.

  1. Delete the cache entry first.
  2. Update the database. Due to master-slave replication lag, the slave database might still hold old data briefly.
  3. After a short delay, delete the cache a second time. This double deletion reduces the probability of dirty data being served to users.

Other strategies include:

  • Distributed Locks: Using a shared read/write lock for strong consistency, though this sacrifices performance.
  • Asynchronous Notification: Using message queues like RabbitMQ or tools like Canal to synchronize data asynchronously, offering higher performence.

Redis Persistence

RDB (Redis Database Backup) creates point-in-time snapshots of the dataset at specified intervals and writes them to disk. The bgsave command forks a child process, which shares memory with the parent via copy-on-write. The child writes the data to an RDB file, while the parent handles ongoing write operations, which copy modified memory pages. On restart, the snapshot is loaded to recover data.

AOF (Append Only File) logs every write operation received by the server. These commands are appended to a file and can be replayed during startup to reconstruct the dataset. AOF file rewriting (bgrewriteaof) creates a compact version by removing redundant commands for the same key. AOF provides better durability than RDB but results in larger files. In production, combining both methods is common.

Data Eviction and Expiry

Redis employs a combination of lazy deletion and active (periodic) deletion to manage expired keys.

  • Lazy Deletion: Checks a key's expiry only when it is accessed and deletes it if expired.
  • Periodic Deletion: Incrementally checks random keys from a subset of databases at a set frequency (default 10 times per second) to remove expired ones.

When memory is exhausted, Redis uses a configurable eviction policy to make room for new data. Common policies include:

  • allkeys-lru: Evicts the least recently used keys out of all keys. Recommended for general use with hot/cold data patterns.
  • volatile-lru: Evicts LRU keys among those with an expiration set.
  • allkeys-random and volatile-random: Evicts random keys for uniform access patterns.
  • volatile-ttl: Evicts keys with the shortest remaining TTL.
  • allkeys-lfu and volatile-lfu: Evicts the least frequently used keys.

If the default noeviction policy is active, new write operations will fail with an error once memory is full.

Core Java Collections

ArrayList vs. LinkedList

  • ArrayList is backed by a dynamic array and offers O(1) random access. Adding or removing elements at the tail is O(1) on average, but inserting or deleting elsewhere requires shifting elements, resulting in O(n) complexity. Memory is a contiguous block.
  • LinkedList is implemented as a doubly-linked list.1 It supports O(1) insertion and deletion at the head or tail.ol finding a node by index requires traversing the list (O(n)). It uses more memory due to storing node pointers.

HashMap Internals

Data Structure (JDK 1.8) An array of nodes, each acting as a bucket. When hash collisions occur, entries are chained in a linked list. If a list length exceeds 8 and the array capacity is at least 64, the list is transformed into a red-black tree to improve search performance from O(n) to O(log n).

Put Operation

  1. If the internal table is empty or null, initializes it via resize().
  2. Computes the bucket index using the key's hash value: (n - 1) & hash.
  3. If the bucket is empty, a new node is placed there.
  4. If a collision occurs:
    • The first node's key is compared. If it matches, the value is overwritten.
    • If the node is a TreeNode, the entry is inserted into the tree.
    • Otherwise, the linked list is traversed. If an existing key is found, the value is updated; if not, a node is appended to the list's end. A tree conversion check follows.
  5. After insertion, if the entry count exceeds the threshold (capacity * load factor), a resize() is triggered.

Resizing Mechanism The1 capacity doubles with each resize. During resizing, entries are redistributed. A node's new position is either the original index or the original index plus the old capacity, determined by checking if (e.hash & oldCap) == 0. The process in JDK 1.8 uses2 a tail-insertion method for linked lists, avoiding the infinite loop problem present in JDK 1.7's head-insertion approach.

Java Concurrency

Thread States

The JDK Thread.State enum defines six states:

  1. NEW: The thread is created but not yet started.
  2. RUNNABLE: A thread executing in the the Java virtual machine.
  3. BLOCKED: A thread blocked waiting for a monitor lock to enter a synchronized block/method.
  4. WAITING: A thread waiting indefinitely for another thread to perform a specific action (e.g., Object.wait(), Thread.join()).
  5. TIMED_WAITING: A thread waiting for a specified amount of time (e.g., Thread.sleep(), Object.wait(timeout)).
  6. TERMINATED: A thread that has completed execution.

The volatile Keyword

volatile ensures two critical properties:

  1. Visibility: A write to a volatile variable is immediately flushed to main memory, and any subsequent read by another thread will see the latest value from main memory, not a stale cached copy.
  2. Ordering: It prevents instruction reordering. Memory barriers are inserted to ensure that reads and writes of the volatile variable are not reordered with other operations.

Thread Pools

A thread pool, defined by ThreadPoolExecutor, manages task execution using these core parameters:

  • corePoolSize: The number of threads kept in the pool, even if idle.
  • maximumPoolSize: The maximum allowed number of threads.
  • keepAliveTime & unit: When the number of threads exceeds corePoolSize, idle excess threads are terminated after this duration.
  • workQueue: A blocking queue (ArrayBlockingQueue, LinkedBlockingQueue, etc.) holding tasks before they are executed by threads.
  • threadFactory: A factory for creating new threads.
  • handler: The rejection policy to apply when the pool is shutdown or at maximum capacity (Abort, CallerRuns, Discard, DiscardOldest).

Execution Flow

  1. If the running thread count is less than corePoolSize, a new thread is created for the task.
  2. Otherwise, the task is queued if the workQueue has capacity.
  3. If the queue is full and the thread count is under maximumPoolSize, a new non-core thread is created.
  4. If the count is at maximumPoolSize and the queue is full, the rejection handler processes the task.

AQS and ReentrantLock

The core of many synchronizers in java.util.concurrent is the AbstractQueuedSynchronizer (AQS). It maintains a single int state (modified via CAS operations) and a FIFO waiting queue for blocked threads.

ReentrantLock is a common AQS implementation. It allows a thread to acquire the same lock multiple times, increasing a hold count internally.

  • Non-fair lock (default): A new thread may try to acquire the lock immediately, potentially ahead of waiting threads.
  • Fair lock: Threads acquire the lock strictly in the order they queued.

JVM Fundamentals

Runtime Data Areas

  • Heap: The main area for storing object instances and arrays. This is the primary target for garbage collection. It is logically divided into Young Generation (Eden, Survivor0, Survivor1) and Old Generation.
  • Method Area (Metaspace): Stores class metadata, static variables, constants, and compiled code. In Java 8, the implementation has moved from permanent generation on the heap to native memory (Metaspace), avoiding GC management for this area.
  • Java Virtual Machine Stack: A thread-private area that models method execution. Each method invocation creates a stack frame containing a local variable array, operand stack, and return address. Stack memory does not require garbage collection.
  • Program Counter Register: A thread-private indicator of the address of the currently executing JVM instruction.

Garbage Collection

Generational Hypothesis Objects are classified by their lifespan and managed in two generations.

  • Minor GC: Occurs when the Young Generation (Eden) fills up. Live objects are copied to the next Survivor space, and their age increments.ram
  • Full GC: A global event that collects garbage across the Young and Old Generations, typically causing a long pause.

GC Algorithms

  • Mark-Sweep: Marks live objects and sweeps the dead ones. This can cause memory fragmentation.
  • Mark-Compact: After marking, live objects are relocated to one end of the memory to eliminate fragmentation.
  • Copying: Used in the Young Generation. Live objects are copied from one semi-space to another, efficiently reclaiming fragmented space.
  • Generational Collection: A combined approach using copying for the Young Generation and mark-compact or mark-sweep for the Old Generation.

Design Patterns in Practice

The Strategy Pattern for Evolving Logic

A common7 business example is handling multiple payment methods or different discount calculations. Instead of if-else branches, a PaymentContext can hold a reference to a PaymentStrategy interface. Concrete strategies like CreditCardStrategy or PayPalStrategy implement the interface, allowing the runtime selection6 of algorithm and making it easy to add new methods without modifying existing, tested code.

The Chain of Responsibility Pattern for Multi-Layer Validation

For complex1 request validation, such as user registration,24 a1 series of handlers can be chained. UsernameValidator, PasswordStrengthValidator, and EmailFormatValidator are3 concrete handlers in a chain. A request is passed to the first handler. If valid, it is forwarded to the next; if invalid, processing stops and an error is returned. This decouples the order and8010 responsibility control code for clean, scalable validation logic.

Tags: java Collections Concurrency JVM Design Patterns

Posted on Sun, 10 May 2026 19:03:48 +0000 by Loki88